diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 9a5aced..0000000 --- a/.gitignore +++ /dev/null @@ -1,139 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) -web_modules/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional stylelint cache -.stylelintcache - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variable files -.env -.env.* -!.env.example - -# parcel-bundler cache (https://parceljs.org/) -.cache -.parcel-cache - -# Next.js build output -.next -out - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# vuepress v2.x temp and cache directory -.temp -.cache - -# Sveltekit cache directory -.svelte-kit/ - -# vitepress build output -**/.vitepress/dist - -# vitepress cache directory -**/.vitepress/cache - -# Docusaurus cache and generated files -.docusaurus - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# Firebase cache directory -.firebase/ - -# TernJS port file -.tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# yarn v3 -.pnp.* -.yarn/* -!.yarn/patches -!.yarn/plugins -!.yarn/releases -!.yarn/sdks -!.yarn/versions - -# Vite logs files -vite.config.js.timestamp-* -vite.config.ts.timestamp-* diff --git a/About.html b/About.html deleted file mode 100644 index ba0cbbc..0000000 --- a/About.html +++ /dev/null @@ -1,112 +0,0 @@ - - - - - - AIDLOOP-FRONTENDWEB - - - - -
- - -
- -
- -

Recruit Verify

- -

- Volun-teer -

- -

for Your Events

- -

- Create and manage volunteers, connect with verified volunteers, - track attendance, and reward effort with certificates — all in one - platform designed to maximize your community impact. -

- -
- - -
- -
- - -
- Volunteer platform preview -
- -
- - - - - - - \ No newline at end of file diff --git a/Admin/certificates/certificate-preview.css b/Admin/certificates/certificate-preview.css new file mode 100644 index 0000000..59e84b6 --- /dev/null +++ b/Admin/certificates/certificate-preview.css @@ -0,0 +1,170 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: "Poppins", sans-serif; +} + +body { + background: #edf2f6; + color: #172033; +} + +.overlay { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 1.5rem; +} + +.modal { + width: 100%; + max-width: 34rem; + background: #ffffff; + border-radius: 1rem; + padding: 1.8rem; + position: relative; + box-shadow: 0 1rem 2.5rem rgba(0, 0, 0, 0.12); +} + +.close-btn { + position: absolute; + top: 1rem; + right: 1rem; + width: 2rem; + height: 2rem; + border: none; + background: transparent; + color: #6b7280; + font-size: 1rem; + cursor: pointer; +} + +.close-btn:hover { + color: #172033; +} + +.modal h1 { + font-size: 1.6rem; + font-weight: 700; + color: #172033; + margin-bottom: 1.5rem; +} + +.details-grid { + display: grid; + grid-template-columns: 8rem 1fr; + gap: 0.9rem 1rem; + margin-bottom: 1.5rem; +} + +.label { + font-size: 0.9rem; + font-weight: 600; + color: #223f6b; +} + +.value { + font-size: 0.9rem; + color: #374151; + word-break: break-word; +} + +.certificate-card { + margin-top: 1rem; + border: 0.0625rem solid #d9dde3; + border-radius: 0.8rem; + padding: 1.4rem 1rem; + text-align: center; + background: #f9fbfd; +} + +.certificate-icon { + position: relative; + width: 4rem; + height: 4rem; + margin: 0 auto 0.8rem; + display: flex; + align-items: center; + justify-content: center; + color: #1d7de2; + font-size: 2rem; +} + +.badge-icon { + position: absolute; + bottom: -0.2rem; + right: -0.2rem; + color: #f2a10d; + font-size: 1.2rem; +} + +.certificate-card p { + font-size: 0.92rem; + font-weight: 700; + color: #16a34a; + letter-spacing: 0.04em; +} + +.feedback { + min-height: 1.2rem; + font-size: 0.85rem; + margin-top: 1rem; + text-align: center; +} + +.feedback.success { + color: #16a34a; +} + +.feedback.error { + color: #dc2626; +} + +.action-row { + margin-top: 1rem; + display: flex; + justify-content: center; +} + +.action-btn { + border: none; + padding: 0.9rem 1.2rem; + border-radius: 0.5rem; + font-size: 0.9rem; + cursor: pointer; +} + +.secondary-btn { + background: #223f6b; + color: #ffffff; +} + +.secondary-btn:hover { + opacity: 0.94; +} + +.secondary-btn:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +@media (max-width: 36rem) { + .modal { + padding: 1.2rem; + } + + .details-grid { + grid-template-columns: 1fr; + gap: 0.4rem; + } + + .label { + margin-top: 0.4rem; + } + + .action-btn { + width: 100%; + } +} \ No newline at end of file diff --git a/Admin/certificates/certificate-preview.html b/Admin/certificates/certificate-preview.html new file mode 100644 index 0000000..a6a53b8 --- /dev/null +++ b/Admin/certificates/certificate-preview.html @@ -0,0 +1,60 @@ + + + + + + Certificate Preview + + + + + + + + +
+ +
+ + + + \ No newline at end of file diff --git a/Admin/certificates/certificate-preview.js b/Admin/certificates/certificate-preview.js new file mode 100644 index 0000000..b1924b6 --- /dev/null +++ b/Admin/certificates/certificate-preview.js @@ -0,0 +1,157 @@ +const API_BASE_URL = "https://aidloop-backend.onrender.com/api"; + +const els = { + closeBtn: document.getElementById("closeBtn"), + volunteerName: document.getElementById("volunteerName"), + eventName: document.getElementById("eventName"), + phoneNumber: document.getElementById("phoneNumber"), + organizerName: document.getElementById("organizerName"), + eventDate: document.getElementById("eventDate"), + certificateStatus: document.getElementById("certificateStatus"), + feedback: document.getElementById("feedback"), + downloadBtn: document.getElementById("downloadBtn") +}; + +const certificateId = new URLSearchParams(window.location.search).get("id"); + +async function apiRequest(endpoint, options = {}) { + const response = await fetch(`${API_BASE_URL}${endpoint}`, { + credentials: "include", + headers: { + ...(options.headers || {}) + }, + ...options + }); + + const contentType = response.headers.get("content-type") || ""; + const data = contentType.includes("application/json") + ? await response.json() + : await response.blob(); + + if (!response.ok) { + if (contentType.includes("application/json")) { + throw new Error(data.message || data.error || "Request failed"); + } + throw new Error("Request failed"); + } + + return data; +} + +function formatDate(dateValue) { + if (!dateValue) return "—"; + const date = new Date(dateValue); + if (Number.isNaN(date.getTime())) return dateValue; + return date.toLocaleDateString("en-GB", { + day: "2-digit", + month: "long", + year: "numeric" + }); +} + +function setFeedback(message, type = "") { + els.feedback.textContent = message; + els.feedback.className = "feedback"; + if (type) { + els.feedback.classList.add(type); + } +} + +function populateCertificate(data) { + els.volunteerName.textContent = + data.user?.fullName || + data.user?.name || + data.volunteer?.fullName || + data.volunteer?.name || + data.volunteerName || + "—"; + + els.eventName.textContent = + data.event?.name || + data.eventName || + "—"; + + els.phoneNumber.textContent = + data.user?.phoneNumber || + data.user?.phone || + data.volunteer?.phoneNumber || + data.volunteer?.phone || + data.phoneNumber || + "—"; + + els.organizerName.textContent = + data.organizer?.fullName || + data.organizer?.name || + data.event?.organizer?.fullName || + data.event?.organizer?.name || + data.organizerName || + "—"; + + els.eventDate.textContent = formatDate( + data.event?.date || + data.eventDate || + data.issuedAt || + data.createdAt + ); + + const status = String(data.status || "issued").toUpperCase(); + els.certificateStatus.textContent = status; +} + +async function loadCertificate() { + if (!certificateId) { + setFeedback("No certificate ID provided.", "error"); + els.downloadBtn.disabled = true; + return; + } + + try { + const data = await apiRequest(`/certificates/verify/${certificateId}`); + populateCertificate(data); + setFeedback("Certificate loaded successfully.", "success"); + } catch (error) { + setFeedback(error.message || "Failed to load certificate.", "error"); + els.downloadBtn.disabled = true; + } +} + +async function downloadCertificate() { + if (!certificateId) return; + + try { + els.downloadBtn.disabled = true; + els.downloadBtn.textContent = "Downloading..."; + + const blob = await apiRequest(`/certificates/download/${certificateId}`); + const url = URL.createObjectURL(blob); + + const link = document.createElement("a"); + link.href = url; + link.download = `certificate-${certificateId}.pdf`; + document.body.appendChild(link); + link.click(); + link.remove(); + + URL.revokeObjectURL(url); + setFeedback("Certificate downloaded successfully.", "success"); + } catch (error) { + setFeedback(error.message || "Failed to download certificate.", "error"); + } finally { + els.downloadBtn.disabled = false; + els.downloadBtn.textContent = "Download Certificate"; + } +} + +function closePreview() { + if (window.history.length > 1) { + window.history.back(); + return; + } + + window.location.href = "certificates.html"; +} + +els.closeBtn.addEventListener("click", closePreview); +els.downloadBtn.addEventListener("click", downloadCertificate); + +document.addEventListener("DOMContentLoaded", loadCertificate); \ No newline at end of file diff --git a/Admin/certificates/certificates.css b/Admin/certificates/certificates.css new file mode 100644 index 0000000..561f437 --- /dev/null +++ b/Admin/certificates/certificates.css @@ -0,0 +1,421 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: "Poppins", sans-serif; +} + +body { + background: #edf2f6; + color: #172033; +} + +.dashboard { + display: flex; + min-height: 100vh; +} + +.sidebar { + width: 15rem; + background: #223f6b; + color: #ffffff; + padding: 1.8rem 0.9rem; +} + +.sidebar-logo { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.35rem; + margin-bottom: 2.4rem; +} + +.sidebar-logo img { + width: 3.1rem; + height: 3.1rem; + object-fit: contain; +} + +.sidebar-menu { + list-style: none; +} + +.sidebar-menu li { + margin-bottom: 0.8rem; +} + +.sidebar-menu li a, +.logout-btn { + display: flex; + align-items: center; + gap: 0.85rem; + text-decoration: none; + color: #ffffff; + padding: 0.9rem 0.85rem; + border-radius: 0.45rem; + font-size: 0.95rem; + transition: background 0.2s ease; + width: 100%; + border: none; + background: transparent; + cursor: pointer; +} + +.sidebar-menu li a i, +.logout-btn i { + width: 1rem; + text-align: center; +} + +.sidebar-menu li.active a, +.sidebar-menu li a:hover { + background: #1d7de2; +} + +.logout-btn:hover { + background: #ef2f2f; +} + +.main-content { + flex: 1; + display: flex; + flex-direction: column; +} + +.topbar { + height: 5.8rem; + background: #ffffff; + border-bottom: 0.0625rem solid #e6e8eb; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 1.8rem; +} + +.search-box { + width: 21rem; + height: 2.9rem; + display: flex; + align-items: center; + gap: 0.75rem; + border: 0.0625rem solid #d9dde3; + border-radius: 0.35rem; + padding: 0 0.9rem; + background: #ffffff; +} + +.search-box i { + color: #6b7280; + font-size: 0.95rem; +} + +.search-box input { + width: 100%; + border: none; + outline: none; + background: transparent; + font-size: 0.95rem; + color: #374151; +} + +.topbar-right { + display: flex; + align-items: center; + gap: 1rem; +} + +.notification { + position: relative; + color: #111827; + font-size: 1.2rem; +} + +.notification-dot { + position: absolute; + top: 0.08rem; + right: -0.12rem; + width: 0.45rem; + height: 0.45rem; + background: #ef2f2f; + border-radius: 50%; +} + +.admin-profile { + display: flex; + align-items: center; + gap: 0.7rem; +} + +.user-avatar { + width: 2.45rem; + height: 2.45rem; + border-radius: 50%; + object-fit: cover; +} + +.admin-text h4 { + font-size: 0.82rem; + font-weight: 600; + color: #172033; + line-height: 1.2; +} + +.admin-text p { + font-size: 0.72rem; + color: #6b7280; + line-height: 1.2; +} + +.admin-chevron { + color: #6b7280; + font-size: 0.7rem; +} + +.page-content { + padding: 4rem 1.4rem 2rem; +} + +.page-header { + margin-bottom: 2rem; +} + +.page-header h1 { + font-size: 2.1rem; + font-weight: 700; + margin-bottom: 0.3rem; +} + +.page-header p { + font-size: 0.78rem; + color: #6b7280; +} + +.filter-buttons { + display: flex; + gap: 0.8rem; + margin-bottom: 1.4rem; + flex-wrap: wrap; +} + +.filter-btn { + border: 0.0625rem solid #d9dde3; + background: #ffffff; + color: #374151; + padding: 0.75rem 1rem; + border-radius: 0.4rem; + font-size: 0.8rem; + cursor: pointer; +} + +.filter-btn.active { + background: #1d7de2; + color: #ffffff; + border-color: #1d7de2; +} + +.table-wrapper { + background: #ffffff; + overflow: hidden; + border-radius: 0.6rem; +} + +table { + width: 100%; + border-collapse: collapse; +} + +thead { + background: #f4f6f8; +} + +th, +td { + text-align: left; + padding: 1rem 1.4rem; +} + +th { + font-size: 0.62rem; + letter-spacing: 0.08em; + color: #7b8492; + font-weight: 700; +} + +td { + font-size: 0.72rem; + color: #172033; + border-top: 0.0625rem solid #eef1f3; +} + +.action-link { + color: #1d7de2; + text-decoration: none; + font-weight: 600; +} + +.action-link:hover { + text-decoration: underline; +} + +.empty-state { + text-align: center; + padding: 3rem 1rem; +} + +.empty-icon { + width: 4rem; + height: 4rem; + margin: 0 auto 1rem; + border-radius: 50%; + background: #e8f0fb; + color: #1d7de2; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.4rem; +} + +.empty-state h2 { + font-size: 1.1rem; + margin-bottom: 0.4rem; +} + +.empty-state p { + color: #6b7280; + font-size: 0.85rem; +} + +/* Logout Modal */ +.hidden { + display: none !important; +} + +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(15, 23, 42, 0.45); + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; + z-index: 999; +} + +.modal-card { + width: 100%; + max-width: 25rem; + background: #ffffff; + border-radius: 1rem; + padding: 1.5rem; + position: relative; + text-align: center; + box-shadow: 0 1rem 2.5rem rgba(0, 0, 0, 0.15); +} + +.modal-close-btn { + position: absolute; + top: 0.7rem; + right: 0.9rem; + border: none; + background: transparent; + font-size: 1.5rem; + color: #6b7280; + cursor: pointer; +} + +.modal-icon-wrap { + width: 3.5rem; + height: 3.5rem; + margin: 0 auto 1rem; + border-radius: 50%; + background: #fee2e2; + color: #dc2626; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.25rem; +} + +.modal-card h3 { + font-size: 1.2rem; + margin-bottom: 0.5rem; + color: #172033; +} + +.modal-card p { + font-size: 0.92rem; + color: #6b7280; + margin-bottom: 1.3rem; +} + +.modal-actions { + display: flex; + gap: 0.8rem; + justify-content: center; +} + +.modal-btn { + border: none; + border-radius: 0.55rem; + padding: 0.8rem 1rem; + font-size: 0.9rem; + cursor: pointer; + min-width: 8rem; +} + +.modal-btn.secondary { + background: #e5e7eb; + color: #172033; +} + +.modal-btn.danger { + background: #dc2626; + color: #ffffff; +} + +@media (max-width: 75rem) { + .table-wrapper { + overflow-x: auto; + } + + table { + min-width: 46rem; + } +} + +@media (max-width: 62rem) { + .sidebar { + display: none; + } + + .topbar { + padding: 0 1rem; + } + + .search-box { + width: 16rem; + } + + .page-content { + padding: 2rem 1rem; + } + + .admin-text, + .admin-chevron { + display: none; + } +} + +@media (max-width: 36rem) { + .search-box { + width: 12rem; + } + + .modal-actions { + flex-direction: column; + } + + .modal-btn { + width: 100%; + } +} \ No newline at end of file diff --git a/Admin/certificates/certificates.html b/Admin/certificates/certificates.html new file mode 100644 index 0000000..034e060 --- /dev/null +++ b/Admin/certificates/certificates.html @@ -0,0 +1,156 @@ + + + + + + AIDLoop - Certificates + + + + + + + + + +
+ + +
+
+ + +
+
+ + +
+ +
+ Admin Avatar +
+

Loading...

+

Admin

+
+ +
+
+
+ +
+ + +
+ + + +
+ +
+ + + + + + + + + + + + + + + + +
VOLUNTEER NAMEEVENT NAMEORGANIZERDATE ISSUEDACTIONS
Loading certificates...
+ + +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/Admin/certificates/certificates.js b/Admin/certificates/certificates.js new file mode 100644 index 0000000..dc1f000 --- /dev/null +++ b/Admin/certificates/certificates.js @@ -0,0 +1,261 @@ +const API_BASE_URL = "https://aidloop-backend.onrender.com/api"; + +const els = { + adminName: document.getElementById("adminName"), + adminRole: document.getElementById("adminRole"), + adminAvatar: document.getElementById("adminAvatar"), + certificatesTable: document.getElementById("certificatesTable"), + certificatesTableWrap: document.getElementById("certificatesTableWrap"), + emptyState: document.getElementById("emptyState"), + searchInput: document.getElementById("searchInput"), + filterButtons: document.querySelectorAll(".filter-btn"), + logoutBtn: document.getElementById("logoutBtn"), + logoutModal: document.getElementById("logoutModal"), + closeLogoutModal: document.getElementById("closeLogoutModal"), + cancelLogout: document.getElementById("cancelLogout"), + confirmLogout: document.getElementById("confirmLogout") +}; + +let certificateRowsCache = []; +let currentFilter = "all"; + +async function apiRequest(endpoint, options = {}) { + const response = await fetch(`${API_BASE_URL}${endpoint}`, { + credentials: "include", + headers: { + "Content-Type": "application/json", + ...(options.headers || {}) + }, + ...options + }); + + const contentType = response.headers.get("content-type") || ""; + const data = contentType.includes("application/json") + ? await response.json() + : await response.blob(); + + if (!response.ok) { + if (contentType.includes("application/json")) { + throw new Error(data.message || data.error || "Request failed"); + } + throw new Error("Request failed"); + } + + return data; +} + +function normalizeCertificates(payload) { + if (Array.isArray(payload)) return payload; + if (Array.isArray(payload?.certificates)) return payload.certificates; + if (Array.isArray(payload?.data)) return payload.data; + return []; +} + +function formatDate(dateValue) { + if (!dateValue) return "—"; + const date = new Date(dateValue); + if (Number.isNaN(date.getTime())) return dateValue; + return date.toLocaleDateString("en-GB", { + day: "2-digit", + month: "short", + year: "numeric" + }); +} + +function getStatusValue(item) { + const raw = String(item.status || "").toLowerCase(); + if (raw === "issued") return "issued"; + return "issued"; +} + +function getVolunteerName(item) { + return ( + item.user?.fullName || + item.user?.name || + item.volunteer?.fullName || + item.volunteer?.name || + item.volunteerName || + "Volunteer" + ); +} + +function getEventName(item) { + return item.event?.name || item.eventName || "Event"; +} + +function getOrganizerName(item) { + return ( + item.organizer?.fullName || + item.organizer?.name || + item.event?.organizer?.fullName || + item.event?.organizer?.name || + item.organizerName || + "Organizer" + ); +} + +function getCertificateId(item) { + return item._id || item.id || item.certificateId || ""; +} + +function openLogoutModal() { + els.logoutModal.classList.remove("hidden"); +} + +function closeLogoutModal() { + els.logoutModal.classList.add("hidden"); + els.confirmLogout.disabled = false; + els.confirmLogout.textContent = "Yes, Log out"; +} + +async function handleLogout() { + try { + els.confirmLogout.disabled = true; + els.confirmLogout.textContent = "Logging out..."; + + await apiRequest("/auth/logout", { + method: "POST" + }); + } catch (error) { + console.warn("Logout failed:", error.message); + } finally { + localStorage.clear(); + sessionStorage.clear(); + window.location.href = "../../index.html"; + } +} + +function renderCertificates() { + const query = els.searchInput.value.trim().toLowerCase(); + + let filtered = [...certificateRowsCache]; + + if (currentFilter !== "all") { + filtered = filtered.filter((item) => { + const status = getStatusValue(item); + return currentFilter === "issued" + ? status === "issued" + : status !== "issued"; + }); + } + + if (query) { + filtered = filtered.filter((item) => { + const searchableText = ` + ${getVolunteerName(item)} + ${getEventName(item)} + ${getOrganizerName(item)} + ${formatDate(item.issuedAt || item.createdAt || item.date)} + `.toLowerCase(); + + return searchableText.includes(query); + }); + } + + if (!filtered.length) { + els.certificatesTableWrap.style.display = "none"; + els.emptyState.style.display = "block"; + return; + } + + els.certificatesTableWrap.style.display = "table"; + els.emptyState.style.display = "none"; + + els.certificatesTable.innerHTML = filtered.map((item) => { + const certificateId = getCertificateId(item); + + return ` + + ${getVolunteerName(item)} + ${getEventName(item)} + ${getOrganizerName(item)} + ${formatDate(item.issuedAt || item.createdAt || item.date)} + + + View Certificate + + + + `; + }).join(""); +} + +async function loadAdminProfile() { + try { + let profile; + try { + profile = await apiRequest("/users/me"); + } catch { + profile = await apiRequest("/user/me"); + } + + els.adminName.textContent = + profile.fullName || + profile.name || + "Admin User"; + + els.adminRole.textContent = + profile.role + ? profile.role.charAt(0).toUpperCase() + profile.role.slice(1) + : "Admin"; + + if (profile.profileImage) { + els.adminAvatar.src = profile.profileImage; + } + } catch (error) { + console.error("Failed to load admin profile:", error.message); + window.location.href = "../profile/admin-profile.html"; + } +} + +async function loadCertificates() { + try { + const payload = await apiRequest("/certificates/my-certificates"); + certificateRowsCache = normalizeCertificates(payload); + renderCertificates(); + } catch (error) { + console.error("Failed to load certificates:", error.message); + certificateRowsCache = []; + renderCertificates(); + } +} + +function bindFilters() { + els.filterButtons.forEach((button) => { + button.addEventListener("click", () => { + els.filterButtons.forEach((btn) => btn.classList.remove("active")); + button.classList.add("active"); + currentFilter = button.dataset.filter; + renderCertificates(); + }); + }); +} + +function bindUI() { + els.searchInput.addEventListener("input", renderCertificates); + + bindFilters(); + + els.logoutBtn.addEventListener("click", openLogoutModal); + els.closeLogoutModal.addEventListener("click", closeLogoutModal); + els.cancelLogout.addEventListener("click", closeLogoutModal); + els.confirmLogout.addEventListener("click", handleLogout); + + els.logoutModal.addEventListener("click", (event) => { + if (event.target === els.logoutModal) { + closeLogoutModal(); + } + }); + + document.addEventListener("keydown", (event) => { + if (event.key === "Escape" && !els.logoutModal.classList.contains("hidden")) { + closeLogoutModal(); + } + }); +} + +document.addEventListener("DOMContentLoaded", async () => { + bindUI(); + await loadAdminProfile(); + await loadCertificates(); +}); \ No newline at end of file diff --git a/Admin/dashboard/admin-dashboard.css b/Admin/dashboard/admin-dashboard.css new file mode 100644 index 0000000..e5a23e6 --- /dev/null +++ b/Admin/dashboard/admin-dashboard.css @@ -0,0 +1,520 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: "Poppins", sans-serif; +} + +body { + background: #edf2f6; + color: #172033; +} + +.dashboard-layout { + display: flex; + min-height: 100vh; +} + +/* Sidebar */ +.sidebar { + width: 15rem; + background: #223f6b; + color: #ffffff; + padding: 1.8rem 0.9rem; +} + +.sidebar-logo { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.35rem; + margin-bottom: 2.4rem; +} + +.sidebar-logo img { + width: 3.1rem; + height: 3.1rem; + object-fit: contain; +} + +.sidebar-logo h2 { + font-size: 1.25rem; + font-weight: 700; + color: #1d7de2; +} + +.sidebar-logo h2 span { + color: #27a657; +} + +.sidebar-menu { + list-style: none; +} + +.sidebar-menu li { + margin-bottom: 0.8rem; +} + +.sidebar-menu li a { + display: flex; + align-items: center; + gap: 0.85rem; + text-decoration: none; + color: #ffffff; + padding: 0.9rem 0.85rem; + border-radius: 0.45rem; + font-size: 0.95rem; + transition: background 0.2s ease; +} + +.sidebar-menu li a i { + width: 1rem; + text-align: center; +} + +.sidebar-menu li.active a, +.sidebar-menu li a:hover { + background: #1d7de2; +} + +/* Logout Button */ +.logout-btn { + width: 100%; + display: flex; + align-items: center; + gap: 0.85rem; + border: none; + background: transparent; + color: #ffffff; + padding: 0.9rem 0.85rem; + border-radius: 0.45rem; + font-size: 0.95rem; + cursor: pointer; + transition: background 0.2s ease; +} + +.logout-btn i { + width: 1rem; + text-align: center; +} + +.logout-btn:hover { + background: #ef2f2f; +} + +/* Main */ +.main-content { + flex: 1; + display: flex; + flex-direction: column; +} + +/* Topbar */ +.topbar { + height: 5.8rem; + background: #ffffff; + border-bottom: 0.0625rem solid #e6e8eb; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 1.8rem; +} + +.search-box { + width: 21rem; + height: 2.9rem; + display: flex; + align-items: center; + gap: 0.75rem; + border: 0.0625rem solid #d9dde3; + border-radius: 0.35rem; + padding: 0 0.9rem; + background: #ffffff; +} + +.search-box i { + color: #6b7280; + font-size: 0.95rem; +} + +.search-box input { + width: 100%; + border: none; + outline: none; + background: transparent; + font-size: 0.95rem; + color: #374151; +} + +.topbar-right { + display: flex; + align-items: center; + gap: 1rem; +} + +.notification { + position: relative; + color: #111827; + font-size: 1.2rem; +} + +.notification-dot { + position: absolute; + top: 0.08rem; + right: -0.12rem; + width: 0.45rem; + height: 0.45rem; + background: #ef2f2f; + border-radius: 50%; +} + +.admin-profile { + display: flex; + align-items: center; + gap: 0.7rem; +} + +.user-avatar { + width: 2.45rem; + height: 2.45rem; + border-radius: 50%; + object-fit: cover; +} + +.admin-text h4 { + font-size: 0.82rem; + font-weight: 600; + color: #172033; + line-height: 1.2; +} + +.admin-text p { + font-size: 0.72rem; + color: #6b7280; + line-height: 1.2; +} + +.admin-chevron { + color: #6b7280; + font-size: 0.7rem; +} + +/* Content */ +.page-content { + padding: 4rem 1.4rem 2rem; +} + +.page-header { + margin-bottom: 2rem; +} + +.page-header h1 { + font-size: 2.1rem; + font-weight: 700; + margin-bottom: 0.3rem; +} + +.page-header p { + font-size: 0.78rem; + color: #6b7280; +} + +/* Stats */ +.stats-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 1.4rem; + margin-bottom: 2.8rem; + max-width: 40rem; +} + +.stat-card { + background: #ffffff; + border: 0.0625rem solid #7f8ea5; + border-radius: 0.6rem; + overflow: hidden; + min-height: 5.8rem; +} + +.stat-value { + display: flex; + align-items: center; + justify-content: center; + height: 3rem; + font-size: 1.8rem; + font-weight: 700; + color: #111827; +} + +.stat-footer { + height: 2.8rem; + border-top: 0.0625rem solid #b8c1cd; + display: flex; + align-items: center; + gap: 0.7rem; + padding: 0 1.1rem; + font-size: 0.65rem; + color: #172033; +} + +.stat-footer i { + color: #1d7de2; + font-size: 1rem; +} + +/* Section titles */ +.quick-actions-section h2, +.recent-activity-section h2 { + font-size: 1.05rem; + font-weight: 700; + margin-bottom: 1.15rem; +} + +/* Quick actions */ +.quick-actions-section { + margin-bottom: 3rem; + max-width: 42rem; +} + +.quick-actions { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 2rem; +} + +.quick-btn { + border: none; + background: #1d7de2; + color: #ffffff; + padding: 0.85rem 1rem; + border-radius: 0.2rem; + font-size: 0.65rem; + cursor: pointer; + transition: opacity 0.2s ease, transform 0.15s ease; +} + +.quick-btn:hover { + opacity: 0.92; +} + +.quick-btn:active { + transform: scale(0.98); +} + +/* Recent activity */ +.recent-activity-section { + max-width: 42rem; +} + +.table-wrapper { + background: #ffffff; + overflow: hidden; +} + +table { + width: 100%; + border-collapse: collapse; +} + +thead { + background: #f4f6f8; +} + +th, +td { + text-align: left; + padding: 1rem 1.4rem; +} + +th { + font-size: 0.62rem; + letter-spacing: 0.08em; + color: #7b8492; + font-weight: 700; +} + +td { + font-size: 0.68rem; + color: #172033; + border-top: 0.0625rem solid #eef1f3; +} + +.status-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 4.7rem; + padding: 0.35rem 0.7rem; + border-radius: 0.45rem; + background: #f2a10d; + color: #ffffff; + font-size: 0.58rem; + font-weight: 700; +} + +.status-badge.pending { + background: #f2a10d; + color: #fff; +} + +.status-badge.published { + background: #16a34a; + color: #fff; +} + +.status-badge.completed { + background: #2563eb; + color: #fff; +} + +/* Logout Modal */ +.hidden { + display: none !important; +} + +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(15, 23, 42, 0.45); + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; + z-index: 999; +} + +.modal-card { + width: 100%; + max-width: 25rem; + background: #ffffff; + border-radius: 1rem; + padding: 1.5rem; + position: relative; + text-align: center; + box-shadow: 0 1rem 2.5rem rgba(0, 0, 0, 0.15); +} + +.modal-close-btn { + position: absolute; + top: 0.7rem; + right: 0.9rem; + border: none; + background: transparent; + font-size: 1.5rem; + color: #6b7280; + cursor: pointer; +} + +.modal-icon-wrap { + width: 3.5rem; + height: 3.5rem; + margin: 0 auto 1rem; + border-radius: 50%; + background: #fee2e2; + color: #dc2626; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.25rem; +} + +.modal-card h3 { + font-size: 1.2rem; + margin-bottom: 0.5rem; + color: #172033; +} + +.modal-card p { + font-size: 0.92rem; + color: #6b7280; + margin-bottom: 1.3rem; +} + +.modal-actions { + display: flex; + gap: 0.8rem; + justify-content: center; +} + +.modal-btn { + border: none; + border-radius: 0.55rem; + padding: 0.8rem 1rem; + font-size: 0.9rem; + cursor: pointer; + min-width: 8rem; +} + +.modal-btn.secondary { + background: #e5e7eb; + color: #172033; +} + +.modal-btn.danger { + background: #dc2626; + color: #ffffff; +} + +/* Responsive */ +@media (max-width: 75rem) { + .stats-grid, + .quick-actions { + grid-template-columns: repeat(2, 1fr); + max-width: none; + } + + .recent-activity-section, + .quick-actions-section { + max-width: none; + } + + .table-wrapper { + overflow-x: auto; + } + + table { + min-width: 42rem; + } +} + +@media (max-width: 62rem) { + .sidebar { + display: none; + } + + .topbar { + padding: 0 1rem; + } + + .search-box { + width: 16rem; + } + + .page-content { + padding: 2rem 1rem; + } + + .admin-text, + .admin-chevron { + display: none; + } +} + +@media (max-width: 36rem) { + .stats-grid, + .quick-actions { + grid-template-columns: 1fr; + } + + .search-box { + width: 12rem; + } + + .modal-actions { + flex-direction: column; + } + + .modal-btn { + width: 100%; + } +} \ No newline at end of file diff --git a/Admin/dashboard/admin-dashboard.html b/Admin/dashboard/admin-dashboard.html new file mode 100644 index 0000000..4b2cf57 --- /dev/null +++ b/Admin/dashboard/admin-dashboard.html @@ -0,0 +1,197 @@ + + + + + + AIDLoop Dashboard + + + + + + + + +
+ + + + +
+ +
+ + +
+
+ + +
+ +
+ Admin Avatar +
+

Loading...

+

Admin

+
+ +
+
+
+ + +
+ + + +
+
+
0
+ +
+ +
+
0
+ +
+ +
+
0
+ +
+ +
+
0
+ +
+
+ + +
+

Quick Actions

+ +
+ + + + + +
+
+ + +
+

Recent Activity

+ +
+ + + + + + + + + + + + + + + +
ACTIVITYENTITYDATESTATUS
Loading recent activity...
+
+
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/Admin/dashboard/admin-dashboard.js b/Admin/dashboard/admin-dashboard.js new file mode 100644 index 0000000..3c88cce --- /dev/null +++ b/Admin/dashboard/admin-dashboard.js @@ -0,0 +1,316 @@ +const API_BASE_URL = "https://aidloop-backend.onrender.com/api"; + +const els = { + adminName: document.getElementById("adminName"), + adminRole: document.getElementById("adminRole"), + adminAvatar: document.getElementById("adminAvatar"), + organizationCount: document.getElementById("organizationCount"), + pendingCount: document.getElementById("pendingCount"), + eventsCount: document.getElementById("eventsCount"), + activeUsersCount: document.getElementById("activeUsersCount"), + activityTable: document.getElementById("activityTable"), + searchInput: document.getElementById("searchInput"), + goVerificationQueue: document.getElementById("goVerificationQueue"), + viewOrganizations: document.getElementById("viewOrganizations"), + viewEvents: document.getElementById("viewEvents"), + logoutBtn: document.getElementById("logoutBtn"), + logoutModal: document.getElementById("logoutModal"), + closeLogoutModal: document.getElementById("closeLogoutModal"), + cancelLogout: document.getElementById("cancelLogout"), + confirmLogout: document.getElementById("confirmLogout") +}; + +let activityRowsCache = []; + +async function apiRequest(endpoint, options = {}) { + const response = await fetch(`${API_BASE_URL}${endpoint}`, { + credentials: "include", + headers: { + "Content-Type": "application/json", + ...(options.headers || {}) + }, + ...options + }); + + const contentType = response.headers.get("content-type") || ""; + const data = contentType.includes("application/json") + ? await response.json() + : await response.text(); + + if (!response.ok) { + throw new Error( + (data && data.message) || + (data && data.error) || + "Request failed" + ); + } + + return data; +} + +function formatDate(dateValue) { + if (!dateValue) return "—"; + const date = new Date(dateValue); + if (Number.isNaN(date.getTime())) return dateValue; + return date.toLocaleDateString("en-GB", { + day: "2-digit", + month: "short", + year: "numeric" + }); +} + +function setStatValue(element, target) { + const safeTarget = Number.isFinite(Number(target)) ? Number(target) : 0; + const duration = 600; + const steps = 24; + const increment = safeTarget / steps; + let current = 0; + let step = 0; + + const timer = setInterval(() => { + step += 1; + current += increment; + + if (step >= steps) { + element.textContent = safeTarget; + clearInterval(timer); + return; + } + + element.textContent = Math.round(current); + }, duration / steps); +} + +function normalizeUsers(usersPayload) { + if (Array.isArray(usersPayload)) return usersPayload; + if (Array.isArray(usersPayload?.users)) return usersPayload.users; + if (Array.isArray(usersPayload?.data)) return usersPayload.data; + return []; +} + +function normalizeEvents(eventsPayload) { + if (Array.isArray(eventsPayload)) return eventsPayload; + if (Array.isArray(eventsPayload?.events)) return eventsPayload.events; + if (Array.isArray(eventsPayload?.data)) return eventsPayload.data; + return []; +} + +function buildRecentActivity(users, events) { + const activities = []; + + users.slice(0, 5).forEach((user) => { + const isOrganizer = String(user.role || "").toLowerCase() === "organizer"; + const isPending = + String(user.status || "").toLowerCase() === "pending" || + String(user.approvalStatus || "").toLowerCase() === "pending"; + + if (isOrganizer && isPending) { + activities.push({ + activity: "Submitted for Verification", + entity: user.fullName || user.name || user.email || "Organizer", + date: user.createdAt || user.updatedAt, + status: "Pending" + }); + } + }); + + events.slice(0, 5).forEach((event) => { + activities.push({ + activity: "Event Created", + entity: event.name || "Untitled Event", + date: event.createdAt || event.date, + status: event.status || "Published" + }); + }); + + return activities + .sort((a, b) => new Date(b.date || 0) - new Date(a.date || 0)) + .slice(0, 8) + .map((item) => ({ + ...item, + formattedDate: formatDate(item.date) + })); +} + +function renderRecentActivity(rows) { + activityRowsCache = rows; + applyActivitySearch(); +} + +function getStatusBadgeClass(status) { + const normalized = String(status).toLowerCase(); + + if (normalized.includes("pending")) return "pending"; + if (normalized.includes("published")) return "published"; + if (normalized.includes("completed")) return "completed"; + if (normalized.includes("approved")) return "published"; + return "pending"; +} + +function applyActivitySearch() { + const query = els.searchInput.value.trim().toLowerCase(); + const filtered = activityRowsCache.filter((row) => + `${row.activity} ${row.entity} ${row.formattedDate} ${row.status}` + .toLowerCase() + .includes(query) + ); + + if (!filtered.length) { + els.activityTable.innerHTML = ` + + No matching activity found. + + `; + return; + } + + els.activityTable.innerHTML = filtered + .map( + (row) => ` + + ${row.activity} + ${row.entity} + ${row.formattedDate} + + + ${row.status} + + + + ` + ) + .join(""); +} + +function openLogoutModal() { + els.logoutModal.classList.remove("hidden"); +} + +function closeLogoutModal() { + els.logoutModal.classList.add("hidden"); + els.confirmLogout.disabled = false; + els.confirmLogout.textContent = "Yes, Log out"; +} + +async function handleLogout() { + try { + els.confirmLogout.disabled = true; + els.confirmLogout.textContent = "Logging out..."; + + await apiRequest("/auth/logout", { + method: "POST" + }); + } catch (error) { + console.warn("Logout failed:", error.message); + } finally { + localStorage.clear(); + sessionStorage.clear(); + window.location.href = "../../index.html"; + } +} + +async function loadAdminProfile() { + try { + let profile; + try { + profile = await apiRequest("/users/me"); + } catch { + profile = await apiRequest("/user/me"); + } + + els.adminName.textContent = + profile.fullName || + profile.name || + "Admin User"; + + els.adminRole.textContent = + profile.role + ? profile.role.charAt(0).toUpperCase() + profile.role.slice(1) + : "Admin"; + + if (profile.profileImage) { + els.adminAvatar.src = profile.profileImage; + } + } catch (error) { + console.error("Failed to load admin profile:", error.message); + window.location.href = "../profile/admin-profile.html"; + } +} + +async function loadDashboardData() { + try { + const [usersPayload, eventsPayload] = await Promise.all([ + apiRequest("/user").catch(() => apiRequest("/users")), + apiRequest("/events") + ]); + + const users = normalizeUsers(usersPayload); + const events = normalizeEvents(eventsPayload); + + const organizers = users.filter( + (user) => String(user.role || "").toLowerCase() === "organizer" + ); + + const pendingOrganizers = organizers.filter((user) => { + const status = String(user.status || "").toLowerCase(); + const approvalStatus = String(user.approvalStatus || "").toLowerCase(); + return status === "pending" || approvalStatus === "pending"; + }); + + const activeUsers = users.filter((user) => user.isActive !== false); + + setStatValue(els.organizationCount, organizers.length); + setStatValue(els.pendingCount, pendingOrganizers.length); + setStatValue(els.eventsCount, events.length); + setStatValue(els.activeUsersCount, activeUsers.length); + + const recentActivity = buildRecentActivity(users, events); + renderRecentActivity(recentActivity); + } catch (error) { + console.error("Failed to load dashboard data:", error.message); + els.activityTable.innerHTML = ` + + Failed to load dashboard data. + + `; + } +} + +function bindUI() { + els.goVerificationQueue.addEventListener("click", () => { + window.location.href = "../verification/verification-queue.html"; + }); + + els.viewOrganizations.addEventListener("click", () => { + window.location.href = "../organizations/organization-directory.html"; + }); + + els.viewEvents.addEventListener("click", () => { + window.location.href = "../events/events-oversight.html"; + }); + + els.searchInput.addEventListener("input", applyActivitySearch); + + els.logoutBtn.addEventListener("click", openLogoutModal); + els.closeLogoutModal.addEventListener("click", closeLogoutModal); + els.cancelLogout.addEventListener("click", closeLogoutModal); + els.confirmLogout.addEventListener("click", handleLogout); + + els.logoutModal.addEventListener("click", (event) => { + if (event.target === els.logoutModal) { + closeLogoutModal(); + } + }); + + document.addEventListener("keydown", (event) => { + if (event.key === "Escape" && !els.logoutModal.classList.contains("hidden")) { + closeLogoutModal(); + } + }); +} + +document.addEventListener("DOMContentLoaded", async () => { + bindUI(); + await loadAdminProfile(); + await loadDashboardData(); +}); \ No newline at end of file diff --git a/Admin/events/event-details.css b/Admin/events/event-details.css new file mode 100644 index 0000000..0abe53c --- /dev/null +++ b/Admin/events/event-details.css @@ -0,0 +1,186 @@ + * { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: "Poppins", sans-serif; +} + +body { + background: #edf2f6; + color: #172033; +} + +.overlay { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 1.5rem; +} + +.modal-card { + width: 100%; + max-width: 42rem; + background: #ffffff; + border-radius: 1rem; + padding: 1.8rem; + box-shadow: 0 1rem 2.5rem rgba(0, 0, 0, 0.12); +} + +.modal-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1.5rem; +} + +.modal-header h1 { + font-size: 1.6rem; + font-weight: 700; + color: #172033; + line-height: 1.3; +} + +.status-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 6rem; + padding: 0.45rem 0.85rem; + border-radius: 0.55rem; + color: #ffffff; + font-size: 0.72rem; + font-weight: 700; + text-transform: capitalize; +} + +.status-badge.published { + background: #16a34a; +} + +.status-badge.cancelled { + background: #dc2626; +} + +.status-badge.draft { + background: #6b7280; +} + +.details-grid { + display: grid; + grid-template-columns: 8rem 1fr; + gap: 0.9rem 1rem; + margin-bottom: 1.6rem; +} + +.label { + font-size: 0.9rem; + font-weight: 600; + color: #223f6b; +} + +.value { + font-size: 0.9rem; + color: #374151; + word-break: break-word; +} + +.description-section h2 { + font-size: 1rem; + font-weight: 700; + margin-bottom: 0.7rem; + color: #172033; +} + +.description-box { + background: #f8fafc; + border: 0.0625rem solid #e5e7eb; + border-radius: 0.75rem; + padding: 1rem; + font-size: 0.9rem; + line-height: 1.7; + color: #374151; +} + +.feedback { + min-height: 1.2rem; + margin-top: 1rem; + text-align: center; + font-size: 0.85rem; +} + +.feedback.success { + color: #16a34a; +} + +.feedback.error { + color: #dc2626; +} + +.action-row { + margin-top: 1.3rem; + display: flex; + justify-content: flex-end; + gap: 0.8rem; +} + +.action-btn { + border: none; + border-radius: 0.55rem; + padding: 0.85rem 1.2rem; + font-size: 0.9rem; + cursor: pointer; +} + +.close-btn { + background: #e5e7eb; + color: #172033; +} + +.flag-btn { + background: #dc2626; + color: #ffffff; +} + +.flag-btn:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +.social-link { + color: #1d7de2; + text-decoration: none; +} + +.social-link:hover { + text-decoration: underline; +} + +@media (max-width: 36rem) { + .modal-card { + padding: 1.2rem; + } + + .modal-header { + flex-direction: column; + align-items: flex-start; + } + + .details-grid { + grid-template-columns: 1fr; + gap: 0.4rem; + } + + .label { + margin-top: 0.4rem; + } + + .action-row { + flex-direction: column; + } + + .action-btn { + width: 100%; + } +} \ No newline at end of file diff --git a/Admin/events/event-details.html b/Admin/events/event-details.html new file mode 100644 index 0000000..fd70c1d --- /dev/null +++ b/Admin/events/event-details.html @@ -0,0 +1,59 @@ + + + + + + Event Details + + + + + + + +
+ +
+ + + + \ No newline at end of file diff --git a/Admin/events/event-details.js b/Admin/events/event-details.js new file mode 100644 index 0000000..097cbd9 --- /dev/null +++ b/Admin/events/event-details.js @@ -0,0 +1,257 @@ +const API_BASE_URL = "https://aidloop-backend.onrender.com/api"; + +const els = { + eventTitle: document.getElementById("eventTitle"), + statusBadge: document.getElementById("statusBadge"), + orgName: document.getElementById("orgName"), + socialLinks: document.getElementById("socialLinks"), + email: document.getElementById("email"), + phoneNumber: document.getElementById("phoneNumber"), + dateTime: document.getElementById("dateTime"), + slotsFilled: document.getElementById("slotsFilled"), + description: document.getElementById("description"), + feedback: document.getElementById("feedback"), + closeBtn: document.getElementById("closeBtn"), + flagBtn: document.getElementById("flagBtn") +}; + +const eventId = new URLSearchParams(window.location.search).get("id"); +let currentEvent = null; + +async function apiRequest(endpoint, options = {}) { + const response = await fetch(`${API_BASE_URL}${endpoint}`, { + credentials: "include", + headers: { + "Content-Type": "application/json", + ...(options.headers || {}) + }, + ...options + }); + + const contentType = response.headers.get("content-type") || ""; + const data = contentType.includes("application/json") + ? await response.json() + : await response.text(); + + if (!response.ok) { + throw new Error( + (data && data.message) || + (data && data.error) || + "Request failed" + ); + } + + return data; +} + +function normalizeEvents(eventsPayload) { + if (Array.isArray(eventsPayload)) return eventsPayload; + if (Array.isArray(eventsPayload?.events)) return eventsPayload.events; + if (Array.isArray(eventsPayload?.data)) return eventsPayload.data; + return []; +} + +function formatDate(dateValue) { + if (!dateValue) return "—"; + const date = new Date(dateValue); + if (Number.isNaN(date.getTime())) return dateValue; + return date.toLocaleDateString("en-GB", { + day: "2-digit", + month: "long", + year: "numeric" + }); +} + +function formatTimeValue(value) { + if (!value) return ""; + if (String(value).includes("AM") || String(value).includes("PM")) return value; + + const [hours, minutes] = String(value).split(":"); + if (!hours || !minutes) return value; + + const hourNum = Number(hours); + const suffix = hourNum >= 12 ? "PM" : "AM"; + const normalizedHour = hourNum % 12 || 12; + return `${normalizedHour}:${minutes} ${suffix}`; +} + +function getStatusValue(event) { + const status = String(event.status || "").toLowerCase(); + + if (status.includes("cancel")) return "cancelled"; + if (status.includes("draft")) return "draft"; + return "published"; +} + +function setStatusBadge(status) { + const normalized = getStatusValue({ status }); + els.statusBadge.textContent = + normalized.charAt(0).toUpperCase() + normalized.slice(1); + + els.statusBadge.className = "status-badge"; + els.statusBadge.classList.add(normalized); +} + +function getOrganizerName(event) { + return ( + event.organizer?.fullName || + event.organizer?.name || + event.organizerName || + "—" + ); +} + +function getOrganizerEmail(event) { + return ( + event.organizer?.email || + event.contactEmail || + event.email || + "—" + ); +} + +function getOrganizerPhone(event) { + return ( + event.organizer?.phoneNumber || + event.organizer?.phone || + event.phoneNumber || + event.phone || + "—" + ); +} + +function getSocialLink(event) { + return ( + event.organizer?.website || + event.organizer?.socialLink || + event.organizer?.socialLinks?.[0] || + event.website || + event.socialLink || + event.socialLinks?.[0] || + "" + ); +} + +function renderSocialLinks(link) { + if (!link) { + els.socialLinks.textContent = "—"; + return; + } + + els.socialLinks.innerHTML = ` + + ${link} + + `; +} + +function getSlotsFilled(event) { + const filled = + event.filledSlots ?? + event.registrationsCount ?? + event.registeredCount ?? + event.attendeesCount ?? + 0; + + const total = event.volunteerSlots ?? event.slots ?? 0; + return `${filled} / ${total}`; +} + +function populateEvent(event) { + els.eventTitle.textContent = event.name || event.title || "Untitled Event"; + setStatusBadge(event.status); + + els.orgName.textContent = getOrganizerName(event); + els.email.textContent = getOrganizerEmail(event); + els.phoneNumber.textContent = getOrganizerPhone(event); + + renderSocialLinks(getSocialLink(event)); + + const formattedDate = formatDate(event.date); + const startTime = formatTimeValue(event.startTime); + const endTime = formatTimeValue(event.endTime); + + els.dateTime.textContent = + startTime && endTime + ? `${formattedDate} • ${startTime} - ${endTime}` + : startTime + ? `${formattedDate} • ${startTime}` + : formattedDate; + + els.slotsFilled.textContent = getSlotsFilled(event); + els.description.textContent = + event.description || "No event description available."; +} + +function setFeedback(message, type = "") { + els.feedback.textContent = message; + els.feedback.className = "feedback"; + if (type) { + els.feedback.classList.add(type); + } +} + +async function loadEventDetails() { + if (!eventId) { + setFeedback("No event ID provided.", "error"); + els.flagBtn.disabled = true; + return; + } + + try { + const payload = await apiRequest("/events"); + const events = normalizeEvents(payload); + + currentEvent = events.find( + (event) => String(event._id || event.id) === String(eventId) + ); + + if (!currentEvent) { + throw new Error("Event not found"); + } + + populateEvent(currentEvent); + } catch (error) { + els.eventTitle.textContent = "Unable to load event"; + els.description.textContent = "Failed to fetch event details."; + setFeedback(error.message || "Failed to load event details.", "error"); + els.flagBtn.disabled = true; + } +} + +async function flagEvent() { + if (!eventId) return; + + try { + els.flagBtn.disabled = true; + els.flagBtn.textContent = "Flagging..."; + + await apiRequest(`/events/${eventId}/cancel`, { + method: "PATCH", + body: JSON.stringify({ + reason: "Flagged by admin" + }) + }); + + setFeedback("Event flagged and cancelled successfully.", "success"); + setStatusBadge("cancelled"); + } catch (error) { + setFeedback(error.message || "Failed to flag event.", "error"); + els.flagBtn.disabled = false; + els.flagBtn.textContent = "Flag Event"; + } +} + +function closeModal() { + if (window.history.length > 1) { + window.history.back(); + return; + } + + window.location.href = "events-oversight.html"; +} + +els.closeBtn.addEventListener("click", closeModal); +els.flagBtn.addEventListener("click", flagEvent); + +document.addEventListener("DOMContentLoaded", loadEventDetails); \ No newline at end of file diff --git a/Admin/events/events-oversight.css b/Admin/events/events-oversight.css new file mode 100644 index 0000000..b2b49a5 --- /dev/null +++ b/Admin/events/events-oversight.css @@ -0,0 +1,445 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: "Poppins", sans-serif; +} + +body { + background: #edf2f6; + color: #172033; +} + +.dashboard { + display: flex; + min-height: 100vh; +} + +.sidebar { + width: 15rem; + background: #223f6b; + color: #ffffff; + padding: 1.8rem 0.9rem; +} + +.sidebar-logo { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.35rem; + margin-bottom: 2.4rem; +} + +.sidebar-logo img { + width: 3.1rem; + height: 3.1rem; + object-fit: contain; +} + +.sidebar-menu { + list-style: none; +} + +.sidebar-menu li { + margin-bottom: 0.8rem; +} + +.sidebar-menu li a, +.logout-btn { + display: flex; + align-items: center; + gap: 0.85rem; + text-decoration: none; + color: #ffffff; + padding: 0.9rem 0.85rem; + border-radius: 0.45rem; + font-size: 0.95rem; + transition: background 0.2s ease; + width: 100%; + border: none; + background: transparent; + cursor: pointer; +} + +.sidebar-menu li a i, +.logout-btn i { + width: 1rem; + text-align: center; +} + +.sidebar-menu li.active a, +.sidebar-menu li a:hover { + background: #1d7de2; +} + +.logout-btn:hover { + background: #ef2f2f; +} + +.main-content { + flex: 1; + display: flex; + flex-direction: column; +} + +.topbar { + height: 5.8rem; + background: #ffffff; + border-bottom: 0.0625rem solid #e6e8eb; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 1.8rem; +} + +.search-box { + width: 21rem; + height: 2.9rem; + display: flex; + align-items: center; + gap: 0.75rem; + border: 0.0625rem solid #d9dde3; + border-radius: 0.35rem; + padding: 0 0.9rem; + background: #ffffff; +} + +.search-box i { + color: #6b7280; + font-size: 0.95rem; +} + +.search-box input { + width: 100%; + border: none; + outline: none; + background: transparent; + font-size: 0.95rem; + color: #374151; +} + +.topbar-right { + display: flex; + align-items: center; + gap: 1rem; +} + +.notification { + position: relative; + color: #111827; + font-size: 1.2rem; +} + +.notification-dot { + position: absolute; + top: 0.08rem; + right: -0.12rem; + width: 0.45rem; + height: 0.45rem; + background: #ef2f2f; + border-radius: 50%; +} + +.admin-profile { + display: flex; + align-items: center; + gap: 0.7rem; +} + +.user-avatar { + width: 2.45rem; + height: 2.45rem; + border-radius: 50%; + object-fit: cover; +} + +.admin-text h4 { + font-size: 0.82rem; + font-weight: 600; + color: #172033; + line-height: 1.2; +} + +.admin-text p { + font-size: 0.72rem; + color: #6b7280; + line-height: 1.2; +} + +.admin-chevron { + color: #6b7280; + font-size: 0.7rem; +} + +.page-content { + padding: 4rem 1.4rem 2rem; +} + +.page-header { + margin-bottom: 2rem; +} + +.page-header h1 { + font-size: 2.1rem; + font-weight: 700; + margin-bottom: 0.3rem; +} + +.page-header p { + font-size: 0.78rem; + color: #6b7280; +} + +.filter-buttons { + display: flex; + gap: 0.8rem; + margin-bottom: 1.4rem; + flex-wrap: wrap; +} + +.filter-btn { + border: 0.0625rem solid #d9dde3; + background: #ffffff; + color: #374151; + padding: 0.75rem 1rem; + border-radius: 0.4rem; + font-size: 0.8rem; + cursor: pointer; +} + +.filter-btn.active { + background: #1d7de2; + color: #ffffff; + border-color: #1d7de2; +} + +.table-wrapper { + background: #ffffff; + overflow: hidden; + border-radius: 0.6rem; +} + +table { + width: 100%; + border-collapse: collapse; +} + +thead { + background: #f4f6f8; +} + +th, +td { + text-align: left; + padding: 1rem 1.4rem; +} + +th { + font-size: 0.62rem; + letter-spacing: 0.08em; + color: #7b8492; + font-weight: 700; +} + +td { + font-size: 0.72rem; + color: #172033; + border-top: 0.0625rem solid #eef1f3; +} + +.status-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 5.2rem; + padding: 0.35rem 0.7rem; + border-radius: 0.45rem; + color: #ffffff; + font-size: 0.58rem; + font-weight: 700; +} + +.status-badge.published { + background: #16a34a; +} + +.status-badge.cancelled { + background: #dc2626; +} + +.status-badge.draft { + background: #6b7280; +} + +.action-link { + color: #1d7de2; + text-decoration: none; + font-weight: 600; +} + +.action-link:hover { + text-decoration: underline; +} + +.empty-state { + text-align: center; + padding: 3rem 1rem; +} + +.empty-icon { + width: 4rem; + height: 4rem; + margin: 0 auto 1rem; + border-radius: 50%; + background: #e8f0fb; + color: #1d7de2; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.4rem; +} + +.empty-state h2 { + font-size: 1.1rem; + margin-bottom: 0.4rem; +} + +.empty-state p { + color: #6b7280; + font-size: 0.85rem; +} + +/* Logout Modal */ +.hidden { + display: none !important; +} + +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(15, 23, 42, 0.45); + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; + z-index: 999; +} + +.modal-card { + width: 100%; + max-width: 25rem; + background: #ffffff; + border-radius: 1rem; + padding: 1.5rem; + position: relative; + text-align: center; + box-shadow: 0 1rem 2.5rem rgba(0, 0, 0, 0.15); +} + +.modal-close-btn { + position: absolute; + top: 0.7rem; + right: 0.9rem; + border: none; + background: transparent; + font-size: 1.5rem; + color: #6b7280; + cursor: pointer; +} + +.modal-icon-wrap { + width: 3.5rem; + height: 3.5rem; + margin: 0 auto 1rem; + border-radius: 50%; + background: #fee2e2; + color: #dc2626; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.25rem; +} + +.modal-card h3 { + font-size: 1.2rem; + margin-bottom: 0.5rem; + color: #172033; +} + +.modal-card p { + font-size: 0.92rem; + color: #6b7280; + margin-bottom: 1.3rem; +} + +.modal-actions { + display: flex; + gap: 0.8rem; + justify-content: center; +} + +.modal-btn { + border: none; + border-radius: 0.55rem; + padding: 0.8rem 1rem; + font-size: 0.9rem; + cursor: pointer; + min-width: 8rem; +} + +.modal-btn.secondary { + background: #e5e7eb; + color: #172033; +} + +.modal-btn.danger { + background: #dc2626; + color: #ffffff; +} + +@media (max-width: 75rem) { + .table-wrapper { + overflow-x: auto; + } + + table { + min-width: 46rem; + } +} + +@media (max-width: 62rem) { + .sidebar { + display: none; + } + + .topbar { + padding: 0 1rem; + } + + .search-box { + width: 16rem; + } + + .page-content { + padding: 2rem 1rem; + } + + .admin-text, + .admin-chevron { + display: none; + } +} + +@media (max-width: 36rem) { + .search-box { + width: 12rem; + } + + .modal-actions { + flex-direction: column; + } + + .modal-btn { + width: 100%; + } +} \ No newline at end of file diff --git a/Admin/events/events-oversight.html b/Admin/events/events-oversight.html new file mode 100644 index 0000000..ca5cbc2 --- /dev/null +++ b/Admin/events/events-oversight.html @@ -0,0 +1,157 @@ + + + + + + AIDLoop - Events Oversight + + + + + + + + +
+ + +
+
+ + +
+
+ + +
+ +
+ Admin Avatar +
+

Loading...

+

Admin

+
+ +
+
+
+ +
+ + +
+ + + +
+ +
+ + + + + + + + + + + + + + + + +
EVENT NAMECONTACT EMAILLOCATIONSTATUSACTIONS
Loading events...
+ + +
+
+
+
+ + + + + + + + + diff --git a/Admin/events/events-oversight.js b/Admin/events/events-oversight.js new file mode 100644 index 0000000..c181244 --- /dev/null +++ b/Admin/events/events-oversight.js @@ -0,0 +1,258 @@ +const API_BASE_URL = "https://aidloop-backend.onrender.com/api"; + +const els = { + adminName: document.getElementById("adminName"), + adminRole: document.getElementById("adminRole"), + adminAvatar: document.getElementById("adminAvatar"), + eventsTable: document.getElementById("eventsTable"), + eventsTableWrap: document.getElementById("eventsTableWrap"), + emptyState: document.getElementById("emptyState"), + searchInput: document.getElementById("searchInput"), + filterButtons: document.querySelectorAll(".filter-btn"), + logoutBtn: document.getElementById("logoutBtn"), + logoutModal: document.getElementById("logoutModal"), + closeLogoutModal: document.getElementById("closeLogoutModal"), + cancelLogout: document.getElementById("cancelLogout"), + confirmLogout: document.getElementById("confirmLogout") +}; + +let eventsCache = []; +let currentFilter = "all"; + +async function apiRequest(endpoint, options = {}) { + const response = await fetch(`${API_BASE_URL}${endpoint}`, { + credentials: "include", + headers: { + "Content-Type": "application/json", + ...(options.headers || {}) + }, + ...options + }); + + const contentType = response.headers.get("content-type") || ""; + const data = contentType.includes("application/json") + ? await response.json() + : await response.text(); + + if (!response.ok) { + throw new Error( + (data && data.message) || + (data && data.error) || + "Request failed" + ); + } + + return data; +} + +function normalizeEvents(eventsPayload) { + if (Array.isArray(eventsPayload)) return eventsPayload; + if (Array.isArray(eventsPayload?.events)) return eventsPayload.events; + if (Array.isArray(eventsPayload?.data)) return eventsPayload.data; + return []; +} + +function formatLocation(event) { + if (typeof event.location === "string" && event.location.trim()) { + return event.location; + } + + if (event.location && typeof event.location === "object") { + return ( + [event.location.venue, event.location.city || event.location.state] + .filter(Boolean) + .join(", ") || "—" + ); + } + + return event.city || event.state || "—"; +} + +function getStatusValue(event) { + const status = String(event.status || "").toLowerCase(); + + if (status.includes("cancel")) return "cancelled"; + if (status.includes("draft")) return "draft"; + return "published"; +} + +function getContactEmail(event) { + return ( + event.organizer?.email || + event.contactEmail || + event.email || + "—" + ); +} + +function getEventTitle(event) { + return event.name || event.title || "Untitled Event"; +} + +function getEventId(event) { + return event._id || event.id || ""; +} + +function renderEvents() { + const query = els.searchInput.value.trim().toLowerCase(); + + let filtered = [...eventsCache]; + + if (currentFilter !== "all") { + filtered = filtered.filter((event) => getStatusValue(event) === currentFilter); + } + + if (query) { + filtered = filtered.filter((event) => { + const searchableText = ` + ${getEventTitle(event)} + ${getContactEmail(event)} + ${formatLocation(event)} + ${getStatusValue(event)} + `.toLowerCase(); + + return searchableText.includes(query); + }); + } + + if (!filtered.length) { + els.eventsTableWrap.style.display = "none"; + els.emptyState.style.display = "block"; + return; + } + + els.eventsTableWrap.style.display = "table"; + els.emptyState.style.display = "none"; + + els.eventsTable.innerHTML = filtered.map((event) => { + const status = getStatusValue(event); + + return ` + + ${getEventTitle(event)} + ${getContactEmail(event)} + ${formatLocation(event)} + + + ${status.charAt(0).toUpperCase() + status.slice(1)} + + + + + View Details + + + + `; + }).join(""); +} + +function bindFilters() { + els.filterButtons.forEach((button) => { + button.addEventListener("click", () => { + els.filterButtons.forEach((btn) => btn.classList.remove("active")); + button.classList.add("active"); + currentFilter = button.dataset.filter; + renderEvents(); + }); + }); +} + +function openLogoutModal() { + els.logoutModal.classList.remove("hidden"); +} + +function closeLogoutModal() { + els.logoutModal.classList.add("hidden"); + els.confirmLogout.disabled = false; + els.confirmLogout.textContent = "Yes, Log out"; +} + +async function handleLogout() { + try { + els.confirmLogout.disabled = true; + els.confirmLogout.textContent = "Logging out..."; + + await apiRequest("/auth/logout", { + method: "POST" + }); + } catch (error) { + console.warn("Logout failed:", error.message); + } finally { + localStorage.clear(); + sessionStorage.clear(); + window.location.href = "../../index.html"; + } +} + +async function loadAdminProfile() { + try { + let profile; + try { + profile = await apiRequest("/users/me"); + } catch { + profile = await apiRequest("/user/me"); + } + + els.adminName.textContent = + profile.fullName || + profile.name || + "Admin User"; + + els.adminRole.textContent = + profile.role + ? profile.role.charAt(0).toUpperCase() + profile.role.slice(1) + : "Admin"; + + if (profile.profileImage) { + els.adminAvatar.src = profile.profileImage; + } + } catch (error) { + console.error("Failed to load admin profile:", error.message); + window.location.href = "../profile/admin-profile.html"; + } +} + +async function loadEvents() { + try { + const payload = await apiRequest("/events"); + eventsCache = normalizeEvents(payload); + renderEvents(); + } catch (error) { + console.error("Failed to load events:", error.message); + els.eventsTable.innerHTML = ` + + Failed to load events. + + `; + } +} + +function bindUI() { + els.searchInput.addEventListener("input", renderEvents); + + bindFilters(); + + els.logoutBtn.addEventListener("click", openLogoutModal); + els.closeLogoutModal.addEventListener("click", closeLogoutModal); + els.cancelLogout.addEventListener("click", closeLogoutModal); + els.confirmLogout.addEventListener("click", handleLogout); + + els.logoutModal.addEventListener("click", (event) => { + if (event.target === els.logoutModal) { + closeLogoutModal(); + } + }); + + document.addEventListener("keydown", (event) => { + if (event.key === "Escape" && !els.logoutModal.classList.contains("hidden")) { + closeLogoutModal(); + } + }); +} + +document.addEventListener("DOMContentLoaded", async () => { + bindUI(); + await loadAdminProfile(); + await loadEvents(); +}); \ No newline at end of file diff --git a/Admin/flags/flag-details.css b/Admin/flags/flag-details.css new file mode 100644 index 0000000..f7e2a6b --- /dev/null +++ b/Admin/flags/flag-details.css @@ -0,0 +1,198 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: "Poppins", sans-serif; +} + +body { + background: #edf2f6; + color: #172033; +} + +.overlay { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 1.5rem; +} + +.modal-card { + width: 100%; + max-width: 40rem; + background: #ffffff; + border-radius: 1rem; + padding: 1.8rem; + position: relative; + box-shadow: 0 1rem 2.5rem rgba(0, 0, 0, 0.12); +} + +.close-btn { + position: absolute; + top: 0.8rem; + right: 1rem; + border: none; + background: transparent; + color: #6b7280; + font-size: 1.5rem; + cursor: pointer; +} + +.close-btn:hover { + color: #172033; +} + +.modal-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1.5rem; +} + +.modal-header h1 { + font-size: 1.6rem; + font-weight: 700; + color: #172033; + line-height: 1.3; +} + +.severity-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 5.5rem; + padding: 0.45rem 0.8rem; + border-radius: 0.55rem; + color: #ffffff; + font-size: 0.72rem; + font-weight: 700; + text-transform: capitalize; +} + +.severity-badge.low { + background: #16a34a; +} + +.severity-badge.medium { + background: #f59e0b; +} + +.severity-badge.high { + background: #dc2626; +} + +.details-grid { + display: grid; + grid-template-columns: 9rem 1fr; + gap: 0.9rem 1rem; + margin-bottom: 1.5rem; +} + +.label { + font-size: 0.9rem; + font-weight: 600; + color: #223f6b; +} + +.value { + font-size: 0.9rem; + color: #374151; + word-break: break-word; +} + +.description-section h2 { + font-size: 1rem; + font-weight: 700; + margin-bottom: 0.7rem; + color: #172033; +} + +.description-box { + background: #f8fafc; + border: 0.0625rem solid #e5e7eb; + border-radius: 0.75rem; + padding: 1rem; + font-size: 0.9rem; + line-height: 1.7; + color: #374151; +} + +.feedback { + min-height: 1.2rem; + margin-top: 1rem; + text-align: center; + font-size: 0.85rem; +} + +.feedback.success { + color: #16a34a; +} + +.feedback.error { + color: #dc2626; +} + +.action-row { + margin-top: 1.3rem; + display: flex; + justify-content: flex-end; +} + +.contact-btn { + border: none; + border-radius: 0.55rem; + padding: 0.85rem 1.2rem; + font-size: 0.9rem; + cursor: pointer; + background: #223f6b; + color: #ffffff; +} + +.contact-btn:hover { + opacity: 0.94; +} + +.contact-btn:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +@media (max-width: 36rem) { + .modal-card { + padding: 1.2rem; + } + + .modal-header { + flex-direction: column; + align-items: flex-start; + } + + .details-grid { + grid-template-columns: 1fr; + gap: 0.4rem; + } + + .label { + margin-top: 0.4rem; + } + + .action-row { + justify-content: stretch; + } + + .contact-btn { + width: 100%; + } +} + + + + + + + + + + diff --git a/Admin/flags/flag-details.html b/Admin/flags/flag-details.html new file mode 100644 index 0000000..575412e --- /dev/null +++ b/Admin/flags/flag-details.html @@ -0,0 +1,51 @@ + + + + + + Flag Details + + + + + + + +
+ +
+ + + + \ No newline at end of file diff --git a/Admin/flags/flag-details.js b/Admin/flags/flag-details.js new file mode 100644 index 0000000..73f8d65 --- /dev/null +++ b/Admin/flags/flag-details.js @@ -0,0 +1,216 @@ +const API_BASE_URL = "https://aidloop-backend.onrender.com/api"; + +const els = { + closeBtn: document.getElementById("closeBtn"), + orgTitle: document.getElementById("orgTitle"), + severityBadge: document.getElementById("severityBadge"), + orgName: document.getElementById("orgName"), + flagReason: document.getElementById("flagReason"), + lastEventCancelled: document.getElementById("lastEventCancelled"), + description: document.getElementById("description"), + feedback: document.getElementById("feedback"), + contactBtn: document.getElementById("contactBtn") +}; + +const organizerId = new URLSearchParams(window.location.search).get("id"); +let currentOrganizer = null; +let currentCancelledEvent = null; + +async function apiRequest(endpoint, options = {}) { + const response = await fetch(`${API_BASE_URL}${endpoint}`, { + credentials: "include", + headers: { + "Content-Type": "application/json", + ...(options.headers || {}) + }, + ...options + }); + + const contentType = response.headers.get("content-type") || ""; + const data = contentType.includes("application/json") + ? await response.json() + : await response.text(); + + if (!response.ok) { + throw new Error( + (data && data.message) || + (data && data.error) || + "Request failed" + ); + } + + return data; +} + +function normalizeUsers(payload) { + if (Array.isArray(payload)) return payload; + if (Array.isArray(payload?.users)) return payload.users; + if (Array.isArray(payload?.data)) return payload.data; + return []; +} + +function normalizeEvents(payload) { + if (Array.isArray(payload)) return payload; + if (Array.isArray(payload?.events)) return payload.events; + if (Array.isArray(payload?.data)) return payload.data; + return []; +} + +function formatDate(dateValue) { + if (!dateValue) return "—"; + const date = new Date(dateValue); + if (Number.isNaN(date.getTime())) return dateValue; + return date.toLocaleDateString("en-GB", { + day: "2-digit", + month: "long", + year: "numeric" + }); +} + +function getOrganizerName(user) { + return user.fullName || user.name || user.organizationName || "Organization"; +} + +function getSeverity(count) { + if (count <= 2) return "low"; + if (count <= 4) return "medium"; + return "high"; +} + +function getSeverityLabel(count) { + const severity = getSeverity(count); + return severity.charAt(0).toUpperCase() + severity.slice(1); +} + +function setSeverity(count) { + const severity = getSeverity(count); + els.severityBadge.textContent = getSeverityLabel(count); + els.severityBadge.className = "severity-badge"; + els.severityBadge.classList.add(severity); +} + +function setFeedback(message, type = "") { + els.feedback.textContent = message; + els.feedback.className = "feedback"; + if (type) { + els.feedback.classList.add(type); + } +} + +function populateDetails(organizer, cancelledEvents) { + const latestCancelled = [...cancelledEvents].sort( + (a, b) => + new Date(b.date || b.updatedAt || b.createdAt || 0) - + new Date(a.date || a.updatedAt || a.createdAt || 0) + )[0]; + + currentOrganizer = organizer; + currentCancelledEvent = latestCancelled || null; + + const cancellationsCount = cancelledEvents.length; + const reason = + latestCancelled?.cancelReason || + latestCancelled?.reason || + "Frequent cancellations"; + + els.orgTitle.textContent = getOrganizerName(organizer); + els.orgName.textContent = getOrganizerName(organizer); + els.flagReason.textContent = reason; + + els.lastEventCancelled.textContent = latestCancelled + ? `${latestCancelled.name || latestCancelled.title || "Untitled Event"} • ${formatDate( + latestCancelled.date || latestCancelled.updatedAt || latestCancelled.createdAt + )}` + : "—"; + + els.description.textContent = + organizer.description || + organizer.bio || + "No organizer description available."; + + setSeverity(cancellationsCount); +} + +async function loadFlagDetails() { + if (!organizerId) { + setFeedback("No organizer ID provided.", "error"); + els.contactBtn.disabled = true; + return; + } + + try { + const [usersPayload, eventsPayload] = await Promise.all([ + apiRequest("/user").catch(() => apiRequest("/users")), + apiRequest("/events") + ]); + + const users = normalizeUsers(usersPayload); + const events = normalizeEvents(eventsPayload); + + const organizer = users.find( + (user) => String(user._id || user.id) === String(organizerId) + ); + + if (!organizer) { + throw new Error("Organizer not found"); + } + + const cancelledEvents = events.filter((event) => { + const eventOrganizerId = String( + event.organizer?._id || + event.organizer?.id || + event.organizerId || + "" + ); + const status = String(event.status || "").toLowerCase(); + return ( + eventOrganizerId === String(organizerId) && + status.includes("cancel") + ); + }); + + if (!cancelledEvents.length) { + throw new Error("No flagged cancelled events found for this organizer"); + } + + populateDetails(organizer, cancelledEvents); + } catch (error) { + els.orgTitle.textContent = "Unable to load flag details"; + els.description.textContent = "Failed to fetch organizer flag details."; + setFeedback(error.message || "Failed to load flag details.", "error"); + els.contactBtn.disabled = true; + } +} + +function contactOrganizer() { + if (!currentOrganizer) return; + + const organizerName = getOrganizerName(currentOrganizer); + const organizerEmail = currentOrganizer.email || ""; + const subject = encodeURIComponent(`AidLoop Flag Review - ${organizerName}`); + const body = encodeURIComponent( + `Hello ${organizerName},\n\nWe are contacting you regarding flagged activity connected to your recent event record on AidLoop.\n\nPlease reply with clarification on the cancelled event and any relevant updates.\n\nThank you.` + ); + + if (!organizerEmail) { + setFeedback("No organizer email available.", "error"); + return; + } + + window.location.href = `mailto:${organizerEmail}?subject=${subject}&body=${body}`; + setFeedback("Opening your email client...", "success"); +} + +function closeModal() { + if (window.history.length > 1) { + window.history.back(); + return; + } + + window.location.href = "flags.html"; +} + +els.closeBtn.addEventListener("click", closeModal); +els.contactBtn.addEventListener("click", contactOrganizer); + +document.addEventListener("DOMContentLoaded", loadFlagDetails); \ No newline at end of file diff --git a/Admin/flags/flags.css b/Admin/flags/flags.css new file mode 100644 index 0000000..be6623a --- /dev/null +++ b/Admin/flags/flags.css @@ -0,0 +1,451 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: "Poppins", sans-serif; +} + +body { + background: #edf2f6; + color: #172033; +} + +.dashboard { + display: flex; + min-height: 100vh; +} + +.sidebar { + width: 15rem; + background: #223f6b; + color: #ffffff; + padding: 1.8rem 0.9rem; +} + +.sidebar-logo { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.35rem; + margin-bottom: 2.4rem; +} + +.sidebar-logo img { + width: 3.1rem; + height: 3.1rem; + object-fit: contain; +} + +.sidebar-menu { + list-style: none; +} + +.sidebar-menu li { + margin-bottom: 0.8rem; +} + +.sidebar-menu li a, +.logout-btn { + display: flex; + align-items: center; + gap: 0.85rem; + text-decoration: none; + color: #ffffff; + padding: 0.9rem 0.85rem; + border-radius: 0.45rem; + font-size: 0.95rem; + transition: background 0.2s ease; + width: 100%; + border: none; + background: transparent; + cursor: pointer; +} + +.sidebar-menu li a i, +.logout-btn i { + width: 1rem; + text-align: center; +} + +.sidebar-menu li.active a, +.sidebar-menu li a:hover { + background: #1d7de2; +} + +.logout-btn:hover { + background: #ef2f2f; +} + +.main-content { + flex: 1; + display: flex; + flex-direction: column; +} + +.topbar { + height: 5.8rem; + background: #ffffff; + border-bottom: 0.0625rem solid #e6e8eb; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 1.8rem; +} + +.search-box { + width: 21rem; + height: 2.9rem; + display: flex; + align-items: center; + gap: 0.75rem; + border: 0.0625rem solid #d9dde3; + border-radius: 0.35rem; + padding: 0 0.9rem; + background: #ffffff; +} + +.search-box i { + color: #6b7280; + font-size: 0.95rem; +} + +.search-box input { + width: 100%; + border: none; + outline: none; + background: transparent; + font-size: 0.95rem; + color: #374151; +} + +.topbar-right { + display: flex; + align-items: center; + gap: 1rem; +} + +.notification { + position: relative; + color: #111827; + font-size: 1.2rem; +} + +.notification-dot { + position: absolute; + top: 0.08rem; + right: -0.12rem; + width: 0.45rem; + height: 0.45rem; + background: #ef2f2f; + border-radius: 50%; +} + +.admin-profile { + display: flex; + align-items: center; + gap: 0.7rem; +} + +.user-avatar { + width: 2.45rem; + height: 2.45rem; + border-radius: 50%; + object-fit: cover; +} + +.admin-text h4 { + font-size: 0.82rem; + font-weight: 600; + color: #172033; + line-height: 1.2; +} + +.admin-text p { + font-size: 0.72rem; + color: #6b7280; + line-height: 1.2; +} + +.admin-chevron { + color: #6b7280; + font-size: 0.7rem; +} + +.page-content { + padding: 4rem 1.4rem 2rem; +} + +.page-header { + margin-bottom: 2rem; + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; +} + +.page-title-group h1 { + font-size: 2.1rem; + font-weight: 700; + margin-bottom: 0.3rem; +} + +.page-title-group p { + font-size: 0.78rem; + color: #6b7280; +} + +.filter-buttons { + display: flex; + gap: 0.8rem; + flex-wrap: wrap; +} + +.filter-btn { + border: 0.0625rem solid #d9dde3; + background: #ffffff; + color: #374151; + padding: 0.75rem 1rem; + border-radius: 0.4rem; + font-size: 0.8rem; + cursor: pointer; +} + +.filter-btn.active { + background: #1d7de2; + color: #ffffff; + border-color: #1d7de2; +} + +.table-wrapper { + background: #ffffff; + overflow: hidden; + border-radius: 0.6rem; +} + +table { + width: 100%; + border-collapse: collapse; +} + +thead { + background: #f4f6f8; +} + +th, +td { + text-align: left; + padding: 1rem 1.4rem; +} + +th { + font-size: 0.62rem; + letter-spacing: 0.08em; + color: #7b8492; + font-weight: 700; +} + +td { + font-size: 0.72rem; + color: #172033; + border-top: 0.0625rem solid #eef1f3; +} + +.severity-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 5rem; + padding: 0.35rem 0.7rem; + border-radius: 0.45rem; + color: #ffffff; + font-size: 0.58rem; + font-weight: 700; +} + +.severity-badge.low { + background: #16a34a; +} + +.severity-badge.medium { + background: #f59e0b; +} + +.severity-badge.high { + background: #dc2626; +} + +.action-link { + color: #1d7de2; + text-decoration: none; + font-weight: 600; +} + +.action-link:hover { + text-decoration: underline; +} + +.empty-state { + text-align: center; + padding: 3rem 1rem; +} + +.empty-icon { + width: 4rem; + height: 4rem; + margin: 0 auto 1rem; + border-radius: 50%; + background: #e8f0fb; + color: #1d7de2; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.4rem; +} + +.empty-state h2 { + font-size: 1.1rem; + margin-bottom: 0.4rem; +} + +.empty-state p { + color: #6b7280; + font-size: 0.85rem; +} + +.hidden { + display: none !important; +} + +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(15, 23, 42, 0.45); + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; + z-index: 999; +} + +.modal-card { + width: 100%; + max-width: 25rem; + background: #ffffff; + border-radius: 1rem; + padding: 1.5rem; + position: relative; + text-align: center; + box-shadow: 0 1rem 2.5rem rgba(0, 0, 0, 0.15); +} + +.modal-close-btn { + position: absolute; + top: 0.7rem; + right: 0.9rem; + border: none; + background: transparent; + font-size: 1.5rem; + color: #6b7280; + cursor: pointer; +} + +.modal-icon-wrap { + width: 3.5rem; + height: 3.5rem; + margin: 0 auto 1rem; + border-radius: 50%; + background: #fee2e2; + color: #dc2626; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.25rem; +} + +.modal-card h3 { + font-size: 1.2rem; + margin-bottom: 0.5rem; + color: #172033; +} + +.modal-card p { + font-size: 0.92rem; + color: #6b7280; + margin-bottom: 1.3rem; +} + +.modal-actions { + display: flex; + gap: 0.8rem; + justify-content: center; +} + +.modal-btn { + border: none; + border-radius: 0.55rem; + padding: 0.8rem 1rem; + font-size: 0.9rem; + cursor: pointer; + min-width: 8rem; +} + +.modal-btn.secondary { + background: #e5e7eb; + color: #172033; +} + +.modal-btn.danger { + background: #dc2626; + color: #ffffff; +} + +@media (max-width: 75rem) { + .table-wrapper { + overflow-x: auto; + } + + table { + min-width: 56rem; + } +} + +@media (max-width: 62rem) { + .sidebar { + display: none; + } + + .topbar { + padding: 0 1rem; + } + + .search-box { + width: 16rem; + } + + .page-content { + padding: 2rem 1rem; + } + + .page-header { + flex-direction: column; + } + + .admin-text, + .admin-chevron { + display: none; + } +} + +@media (max-width: 36rem) { + .search-box { + width: 12rem; + } + + .modal-actions { + flex-direction: column; + } + + .modal-btn { + width: 100%; + } +} \ No newline at end of file diff --git a/Admin/flags/flags.html b/Admin/flags/flags.html new file mode 100644 index 0000000..43b60ad --- /dev/null +++ b/Admin/flags/flags.html @@ -0,0 +1,153 @@ + + + + + + AIDLoop - Flags + + + + + + + + +
+ + +
+
+ + +
+
+ + +
+ +
+ Admin Avatar +
+

Loading...

+

Admin

+
+ +
+
+
+ +
+ + +
+ + + + + + + + + + + + + + + + + +
ORGANIZATION NAMENUMBER OF CANCELLATIONSSEVERITYLAST EVENT DATEFLAG REASONACTIONS
Loading flags...
+ + +
+
+
+
+ + + + + + diff --git a/Admin/flags/flags.js b/Admin/flags/flags.js new file mode 100644 index 0000000..22b33d5 --- /dev/null +++ b/Admin/flags/flags.js @@ -0,0 +1,317 @@ +const API_BASE_URL = "https://aidloop-backend.onrender.com/api"; + +const els = { + adminName: document.getElementById("adminName"), + adminRole: document.getElementById("adminRole"), + adminAvatar: document.getElementById("adminAvatar"), + flagsTable: document.getElementById("flagsTable"), + flagsTableWrap: document.querySelector(".table-wrapper table"), + emptyState: document.getElementById("emptyState"), + searchInput: document.getElementById("searchInput"), + filterButtons: document.querySelectorAll(".filter-btn"), + logoutBtn: document.getElementById("logoutBtn"), + logoutModal: document.getElementById("logoutModal"), + closeLogoutModal: document.getElementById("closeLogoutModal"), + cancelLogout: document.getElementById("cancelLogout"), + confirmLogout: document.getElementById("confirmLogout") +}; + +let flagsCache = []; +let currentFilter = "all"; + +async function apiRequest(endpoint, options = {}) { + const response = await fetch(`${API_BASE_URL}${endpoint}`, { + credentials: "include", + headers: { + "Content-Type": "application/json", + ...(options.headers || {}) + }, + ...options + }); + + const contentType = response.headers.get("content-type") || ""; + const data = contentType.includes("application/json") + ? await response.json() + : await response.text(); + + if (!response.ok) { + throw new Error( + (data && data.message) || + (data && data.error) || + "Request failed" + ); + } + + return data; +} + +function normalizeUsers(payload) { + if (Array.isArray(payload)) return payload; + if (Array.isArray(payload?.users)) return payload.users; + if (Array.isArray(payload?.data)) return payload.data; + return []; +} + +function normalizeEvents(payload) { + if (Array.isArray(payload)) return payload; + if (Array.isArray(payload?.events)) return payload.events; + if (Array.isArray(payload?.data)) return payload.data; + return []; +} + +function formatDate(dateValue) { + if (!dateValue) return "—"; + const date = new Date(dateValue); + if (Number.isNaN(date.getTime())) return dateValue; + return date.toLocaleDateString("en-GB", { + day: "2-digit", + month: "short", + year: "numeric" + }); +} + +function getOrganizerStatus(user) { + const status = String(user.status || "").toLowerCase(); + const approvalStatus = String(user.approvalStatus || "").toLowerCase(); + const isVerified = Boolean(user.isVerified); + + if (status === "rejected" || approvalStatus === "rejected") return "rejected"; + if ( + status === "verified" || + status === "approved" || + approvalStatus === "verified" || + approvalStatus === "approved" || + isVerified + ) { + return "verified"; + } + return "pending"; +} + +function getSeverity(count) { + if (count <= 2) return "low"; + if (count <= 4) return "medium"; + return "high"; +} + +function getSeverityLabel(count) { + const severity = getSeverity(count); + return severity.charAt(0).toUpperCase() + severity.slice(1); +} + +function getOrganizerName(user) { + return user.fullName || user.name || user.organizationName || "Organization"; +} + +function buildFlags(users, events) { + const organizers = users.filter( + (user) => String(user.role || "").toLowerCase() === "organizer" + ); + + return organizers + .map((organizer) => { + const organizerId = String(organizer._id || organizer.id || ""); + + const organizerEvents = events.filter((event) => { + const eventOrganizerId = + String(event.organizer?._id || event.organizer?.id || event.organizerId || ""); + return eventOrganizerId === organizerId; + }); + + const cancelledEvents = organizerEvents.filter((event) => { + const status = String(event.status || "").toLowerCase(); + return status.includes("cancel"); + }); + + if (!cancelledEvents.length) return null; + + const latestCancelled = cancelledEvents.sort( + (a, b) => new Date(b.date || b.updatedAt || b.createdAt || 0) - new Date(a.date || a.updatedAt || a.createdAt || 0) + )[0]; + + return { + id: organizerId, + status: getOrganizerStatus(organizer), + name: getOrganizerName(organizer), + cancellations: cancelledEvents.length, + severity: getSeverity(cancelledEvents.length), + severityLabel: getSeverityLabel(cancelledEvents.length), + lastEventDate: latestCancelled?.date || latestCancelled?.updatedAt || latestCancelled?.createdAt || "", + reason: + latestCancelled?.cancelReason || + latestCancelled?.reason || + "Frequent cancellations" + }; + }) + .filter(Boolean) + .sort((a, b) => new Date(b.lastEventDate || 0) - new Date(a.lastEventDate || 0)); +} + +function renderFlags() { + const query = els.searchInput.value.trim().toLowerCase(); + + let filtered = [...flagsCache]; + + if (currentFilter !== "all") { + filtered = filtered.filter((item) => item.status === currentFilter); + } + + if (query) { + filtered = filtered.filter((item) => { + const searchableText = ` + ${item.name} + ${item.cancellations} + ${item.severityLabel} + ${formatDate(item.lastEventDate)} + ${item.reason} + `.toLowerCase(); + + return searchableText.includes(query); + }); + } + + if (!filtered.length) { + els.flagsTableWrap.style.display = "none"; + els.emptyState.style.display = "block"; + return; + } + + els.flagsTableWrap.style.display = "table"; + els.emptyState.style.display = "none"; + + els.flagsTable.innerHTML = filtered.map((item) => ` + + ${item.name} + ${item.cancellations} + + + ${item.severityLabel} + + + ${formatDate(item.lastEventDate)} + ${item.reason} + + + Review + + + + `).join(""); +} + +function bindFilters() { + els.filterButtons.forEach((button) => { + button.addEventListener("click", () => { + els.filterButtons.forEach((btn) => btn.classList.remove("active")); + button.classList.add("active"); + currentFilter = button.dataset.filter; + renderFlags(); + }); + }); +} + +function openLogoutModal() { + els.logoutModal.classList.remove("hidden"); +} + +function closeLogoutModal() { + els.logoutModal.classList.add("hidden"); + els.confirmLogout.disabled = false; + els.confirmLogout.textContent = "Yes, Log out"; +} + +async function handleLogout() { + try { + els.confirmLogout.disabled = true; + els.confirmLogout.textContent = "Logging out..."; + + await apiRequest("/auth/logout", { + method: "POST" + }); + } catch (error) { + console.warn("Logout failed:", error.message); + } finally { + localStorage.clear(); + sessionStorage.clear(); + window.location.href = "../../index.html"; + } +} + +async function loadAdminProfile() { + try { + let profile; + try { + profile = await apiRequest("/users/me"); + } catch { + profile = await apiRequest("/user/me"); + } + + els.adminName.textContent = + profile.fullName || + profile.name || + "Admin User"; + + els.adminRole.textContent = + profile.role + ? profile.role.charAt(0).toUpperCase() + profile.role.slice(1) + : "Admin"; + + if (profile.profileImage) { + els.adminAvatar.src = profile.profileImage; + } + } catch (error) { + console.error("Failed to load admin profile:", error.message); + window.location.href = "../profile/admin-profile.html"; + } +} + +async function loadFlags() { + try { + const [usersPayload, eventsPayload] = await Promise.all([ + apiRequest("/user").catch(() => apiRequest("/users")), + apiRequest("/events") + ]); + + const users = normalizeUsers(usersPayload); + const events = normalizeEvents(eventsPayload); + + flagsCache = buildFlags(users, events); + renderFlags(); + } catch (error) { + console.error("Failed to load flags:", error.message); + els.flagsTable.innerHTML = ` + + Failed to load flags. + + `; + } +} + +function bindUI() { + els.searchInput.addEventListener("input", renderFlags); + + bindFilters(); + + els.logoutBtn.addEventListener("click", openLogoutModal); + els.closeLogoutModal.addEventListener("click", closeLogoutModal); + els.cancelLogout.addEventListener("click", closeLogoutModal); + els.confirmLogout.addEventListener("click", handleLogout); + + els.logoutModal.addEventListener("click", (event) => { + if (event.target === els.logoutModal) { + closeLogoutModal(); + } + }); + + document.addEventListener("keydown", (event) => { + if (event.key === "Escape" && !els.logoutModal.classList.contains("hidden")) { + closeLogoutModal(); + } + }); +} + +document.addEventListener("DOMContentLoaded", async () => { + bindUI(); + await loadAdminProfile(); + await loadFlags(); +}); \ No newline at end of file diff --git a/Admin/login/admin-login.css b/Admin/login/admin-login.css new file mode 100644 index 0000000..d328716 --- /dev/null +++ b/Admin/login/admin-login.css @@ -0,0 +1,210 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: "Poppins", sans-serif; +} + +body { + background: #edf2f6; + color: #172033; +} + +.login-page { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 1.5rem; +} + +.login-card { + width: 100%; + max-width: 31rem; + background: #ffffff; + border-radius: 1rem; + padding: 2rem; + box-shadow: 0 1rem 2.5rem rgba(0, 0, 0, 0.08); +} + +.logo-wrap { + text-align: center; + margin-bottom: 1.5rem; +} + +.logo { + width: 4rem; + height: 4rem; + object-fit: contain; + margin-bottom: 0.75rem; +} + +.tagline { + font-size: 0.85rem; + color: #6b7280; +} + +.login-card h1 { + font-size: 1.7rem; + font-weight: 700; + color: #172033; + margin-bottom: 1.5rem; + text-align: center; +} + +.form-group { + margin-bottom: 1rem; +} + +.form-group label { + display: block; + font-size: 0.9rem; + font-weight: 600; + color: #223f6b; + margin-bottom: 0.45rem; +} + +.form-group input { + width: 100%; + height: 3rem; + border: 0.0625rem solid #d9dde3; + border-radius: 0.55rem; + padding: 0 0.9rem; + font-size: 0.95rem; + outline: none; + background: #ffffff; + color: #374151; +} + +.form-group input:focus { + border-color: #1d7de2; + box-shadow: 0 0 0 0.15rem rgba(29, 125, 226, 0.12); +} + +.password-wrap { + position: relative; +} + +.password-wrap input { + padding-right: 3rem; +} + +.toggle-password { + position: absolute; + top: 50%; + right: 0.8rem; + transform: translateY(-50%); + border: none; + background: transparent; + color: #6b7280; + cursor: pointer; + font-size: 1rem; +} + +.remember-wrap { + display: inline-flex; + align-items: center; + gap: 0.55rem; + cursor: pointer; + user-select: none; + margin: 0.35rem 0 1rem; +} + +.remember-wrap input { + display: none; +} + +.custom-check { + width: 1rem; + height: 1rem; + border: 0.0625rem solid #c5ccd6; + border-radius: 0.2rem; + display: inline-flex; + align-items: center; + justify-content: center; + background: #ffffff; + color: transparent; + font-size: 0.65rem; +} + +.remember-wrap input:checked + .custom-check { + background: #1d7de2; + border-color: #1d7de2; + color: #ffffff; +} + +.remember-text { + font-size: 0.85rem; + color: #4b5563; +} + +.error-message, +.success-message { + display: block; + min-height: 1rem; + font-size: 0.78rem; + margin-top: 0.35rem; +} + +.error-message { + color: #dc2626; +} + +.success-message { + color: #16a34a; +} + +.form-error, +.success-message { + text-align: center; + margin-bottom: 0.5rem; +} + +.button-group { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.8rem; + margin-top: 0.5rem; +} + +.btn { + height: 3rem; + border: none; + border-radius: 0.55rem; + font-size: 0.92rem; + font-weight: 600; + cursor: pointer; +} + +.btn-primary { + background: #1d7de2; + color: #ffffff; +} + +.btn-primary:hover { + opacity: 0.94; +} + +.btn-secondary { + background: #e5e7eb; + color: #172033; +} + +.btn-secondary:hover { + background: #dfe3e8; +} + +.btn:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +@media (max-width: 32rem) { + .login-card { + padding: 1.3rem; + } + + .button-group { + grid-template-columns: 1fr; + } +} \ No newline at end of file diff --git a/Admin/login/admin-login.html b/Admin/login/admin-login.html new file mode 100644 index 0000000..c821120 --- /dev/null +++ b/Admin/login/admin-login.html @@ -0,0 +1,81 @@ + + + + + + AIDLoop Admin Login + + + + + + + + +
+ +
+ + + + \ No newline at end of file diff --git a/Admin/login/admin-login.js b/Admin/login/admin-login.js new file mode 100644 index 0000000..ff31099 --- /dev/null +++ b/Admin/login/admin-login.js @@ -0,0 +1,147 @@ +const API_BASE_URL = "https://aidloop-backend.onrender.com/api"; + +const els = { + loginForm: document.getElementById("loginForm"), + email: document.getElementById("email"), + password: document.getElementById("password"), + rememberMe: document.getElementById("rememberMe"), + loginBtn: document.getElementById("loginBtn"), + forgotPasswordBtn: document.getElementById("forgotPasswordBtn"), + togglePassword: document.getElementById("togglePassword"), + emailError: document.getElementById("emailError"), + passwordError: document.getElementById("passwordError"), + formError: document.getElementById("formError"), + formSuccess: document.getElementById("formSuccess") +}; + +async function apiRequest(endpoint, options = {}) { + const response = await fetch(`${API_BASE_URL}${endpoint}`, { + credentials: "include", + headers: { + "Content-Type": "application/json", + ...(options.headers || {}) + }, + ...options + }); + + const contentType = response.headers.get("content-type") || ""; + const data = contentType.includes("application/json") + ? await response.json() + : await response.text(); + + if (!response.ok) { + throw new Error( + (data && data.message) || + (data && data.error) || + "Request failed" + ); + } + + return data; +} + +function clearErrors() { + els.emailError.textContent = ""; + els.passwordError.textContent = ""; + els.formError.textContent = ""; + els.formSuccess.textContent = ""; +} + +function validateEmail(value) { + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value); +} + +function validateForm() { + clearErrors(); + + const email = els.email.value.trim(); + const password = els.password.value.trim(); + let isValid = true; + + if (!email) { + els.emailError.textContent = "Email address is required."; + isValid = false; + } else if (!validateEmail(email)) { + els.emailError.textContent = "Enter a valid email address."; + isValid = false; + } + + if (!password) { + els.passwordError.textContent = "Password is required."; + isValid = false; + } + + return isValid; +} + +function restoreRememberedEmail() { + const rememberedEmail = localStorage.getItem("aidloop_admin_email"); + if (rememberedEmail) { + els.email.value = rememberedEmail; + els.rememberMe.checked = true; + } +} + +function togglePasswordVisibility() { + const isPassword = els.password.type === "password"; + els.password.type = isPassword ? "text" : "password"; + + els.togglePassword.innerHTML = isPassword + ? '' + : ''; +} + +async function handleLogin(event) { + event.preventDefault(); + + if (!validateForm()) return; + + try { + clearErrors(); + els.loginBtn.disabled = true; + els.loginBtn.textContent = "Logging in..."; + + const payload = await apiRequest("/auth/login", { + method: "POST", + body: JSON.stringify({ + email: els.email.value.trim(), + password: els.password.value.trim() + }) + }); + + const role = String(payload?.user?.role || payload?.role || "").toLowerCase(); + + if (role && role !== "admin") { + throw new Error("This account is not an admin account."); + } + + if (els.rememberMe.checked) { + localStorage.setItem("aidloop_admin_email", els.email.value.trim()); + } else { + localStorage.removeItem("aidloop_admin_email"); + } + + els.formSuccess.textContent = payload.message || "Login successful."; + + setTimeout(() => { + window.location.href = "../dashboard/admin-dashboard.html"; + }, 800); + } catch (error) { + els.formError.textContent = error.message || "Login failed."; + } finally { + els.loginBtn.disabled = false; + els.loginBtn.textContent = "Log in"; + } +} + +function handleForgotPassword() { + clearErrors(); + els.formError.textContent = + "No admin forgot-password endpoint has been provided yet."; +} + +els.loginForm.addEventListener("submit", handleLogin); +els.togglePassword.addEventListener("click", togglePasswordVisibility); +els.forgotPasswordBtn.addEventListener("click", handleForgotPassword); + +document.addEventListener("DOMContentLoaded", restoreRememberedEmail); \ No newline at end of file diff --git a/Admin/organizations/organization-details.css b/Admin/organizations/organization-details.css new file mode 100644 index 0000000..3d1c6d4 --- /dev/null +++ b/Admin/organizations/organization-details.css @@ -0,0 +1,149 @@ + * { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: "Poppins", sans-serif; +} + +body { + background: #edf2f6; + color: #172033; +} + +.overlay { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 1.5rem; +} + +.modal-card { + width: 100%; + max-width: 42rem; + background: #ffffff; + border-radius: 1rem; + padding: 1.8rem; + position: relative; + box-shadow: 0 1rem 2.5rem rgba(0, 0, 0, 0.12); +} + +.close-btn { + position: absolute; + top: 0.8rem; + right: 1rem; + border: none; + background: transparent; + color: #6b7280; + font-size: 1.5rem; + cursor: pointer; +} + +.close-btn:hover { + color: #172033; +} + +.modal-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1.5rem; +} + +.modal-header h1 { + font-size: 1.6rem; + font-weight: 700; + color: #172033; + line-height: 1.3; +} + +.status-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 7rem; + padding: 0.45rem 0.85rem; + border-radius: 0.55rem; + color: #ffffff; + font-size: 0.72rem; + font-weight: 700; + text-transform: capitalize; +} + +.status-badge.awaiting { + background: #f59e0b; +} + +.status-badge.verified { + background: #16a34a; +} + +.status-badge.rejected { + background: #dc2626; +} + +.details-grid { + display: grid; + grid-template-columns: 8rem 1fr; + gap: 0.9rem 1rem; + margin-bottom: 1.5rem; +} + +.label { + font-size: 0.9rem; + font-weight: 600; + color: #223f6b; +} + +.value { + font-size: 0.9rem; + color: #374151; + word-break: break-word; +} + +.description-section h2 { + font-size: 1rem; + font-weight: 700; + margin-bottom: 0.7rem; + color: #172033; +} + +.description-box { + background: #f8fafc; + border: 0.0625rem solid #e5e7eb; + border-radius: 0.75rem; + padding: 1rem; + font-size: 0.9rem; + line-height: 1.7; + color: #374151; +} + +.social-link { + color: #1d7de2; + text-decoration: none; +} + +.social-link:hover { + text-decoration: underline; +} + +@media (max-width: 36rem) { + .modal-card { + padding: 1.2rem; + } + + .modal-header { + flex-direction: column; + align-items: flex-start; + } + + .details-grid { + grid-template-columns: 1fr; + gap: 0.4rem; + } + + .label { + margin-top: 0.4rem; + } +} \ No newline at end of file diff --git a/Admin/organizations/organization-details.html b/Admin/organizations/organization-details.html new file mode 100644 index 0000000..f68ad8b --- /dev/null +++ b/Admin/organizations/organization-details.html @@ -0,0 +1,51 @@ + + + + + + Organizer Details + + + + + + + +
+ +
+ + + + \ No newline at end of file diff --git a/Admin/organizations/organization-details.js b/Admin/organizations/organization-details.js new file mode 100644 index 0000000..d717ced --- /dev/null +++ b/Admin/organizations/organization-details.js @@ -0,0 +1,183 @@ +const API_BASE_URL = "https://aidloop-backend.onrender.com/api"; + +const els = { + closeBtn: document.getElementById("closeBtn"), + orgTitle: document.getElementById("orgTitle"), + statusBadge: document.getElementById("statusBadge"), + orgName: document.getElementById("orgName"), + socialLinks: document.getElementById("socialLinks"), + email: document.getElementById("email"), + phoneNumber: document.getElementById("phoneNumber"), + location: document.getElementById("location"), + description: document.getElementById("description") +}; + +const organizerId = new URLSearchParams(window.location.search).get("id"); + +async function apiRequest(endpoint, options = {}) { + const response = await fetch(`${API_BASE_URL}${endpoint}`, { + credentials: "include", + headers: { + "Content-Type": "application/json", + ...(options.headers || {}) + }, + ...options + }); + + const contentType = response.headers.get("content-type") || ""; + const data = contentType.includes("application/json") + ? await response.json() + : await response.text(); + + if (!response.ok) { + throw new Error( + (data && data.message) || + (data && data.error) || + "Request failed" + ); + } + + return data; +} + +function normalizeUsers(payload) { + if (Array.isArray(payload)) return payload; + if (Array.isArray(payload?.users)) return payload.users; + if (Array.isArray(payload?.data)) return payload.data; + return []; +} + +function getDisplayName(user) { + return user.fullName || user.name || user.organizationName || "Organization"; +} + +function getLocation(user) { + if (typeof user.location === "string" && user.location.trim()) { + return user.location; + } + + if (user.location && typeof user.location === "object") { + return ( + [user.location.venue, user.location.city || user.location.state] + .filter(Boolean) + .join(", ") || "—" + ); + } + + return user.city || user.state || "—"; +} + +function getOrganizerStatus(user) { + const status = String(user.status || "").toLowerCase(); + const approvalStatus = String(user.approvalStatus || "").toLowerCase(); + const isVerified = Boolean(user.isVerified); + + if (status === "rejected" || approvalStatus === "rejected") return "rejected"; + + if ( + status === "verified" || + status === "approved" || + approvalStatus === "verified" || + approvalStatus === "approved" || + isVerified + ) { + return "verified"; + } + + return "awaiting"; +} + +function setStatusBadge(status) { + els.statusBadge.textContent = + status === "verified" + ? "Verified" + : status === "rejected" + ? "Rejected" + : "Awaiting"; + + els.statusBadge.className = "status-badge"; + els.statusBadge.classList.add(status); +} + +function renderSocialLinks(user) { + const link = + user.website || + user.socialLink || + user.socialLinks?.[0] || + ""; + + if (!link) { + els.socialLinks.textContent = "—"; + return; + } + + els.socialLinks.innerHTML = ` + + ${link} + + `; +} + +function populateOrganizer(user) { + const status = getOrganizerStatus(user); + + els.orgTitle.textContent = getDisplayName(user); + els.orgName.textContent = getDisplayName(user); + els.email.textContent = user.email || "—"; + els.phoneNumber.textContent = user.phoneNumber || user.phone || "—"; + els.location.textContent = getLocation(user); + els.description.innerHTML = `

${ + user.description || + user.bio || + "No organization description available." + }

`; + + renderSocialLinks(user); + setStatusBadge(status); +} + +async function loadOrganizerDetails() { + if (!organizerId) { + els.orgTitle.textContent = "No organizer selected"; + els.description.innerHTML = "

No organizer ID provided.

"; + return; + } + + try { + let payload; + + try { + payload = await apiRequest("/user"); + } catch { + payload = await apiRequest("/users"); + } + + const users = normalizeUsers(payload); + + const organizer = users.find( + (user) => String(user._id || user.id) === String(organizerId) + ); + + if (!organizer) { + throw new Error("Organizer not found"); + } + + populateOrganizer(organizer); + } catch (error) { + els.orgTitle.textContent = "Unable to load organizer"; + els.description.innerHTML = `

${error.message || "Failed to fetch organizer details."}

`; + } +} + +function closeModal() { + if (window.history.length > 1) { + window.history.back(); + return; + } + + window.location.href = "organization-directory.html"; +} + +els.closeBtn.addEventListener("click", closeModal); + +document.addEventListener("DOMContentLoaded", loadOrganizerDetails); \ No newline at end of file diff --git a/Admin/organizations/organization-directory.css b/Admin/organizations/organization-directory.css new file mode 100644 index 0000000..f1bff77 --- /dev/null +++ b/Admin/organizations/organization-directory.css @@ -0,0 +1,453 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: "Poppins", sans-serif; +} + +body { + background: #edf2f6; + color: #172033; +} + +.dashboard { + display: flex; + min-height: 100vh; +} + +.sidebar { + width: 15rem; + background: #223f6b; + color: #ffffff; + padding: 1.8rem 0.9rem; +} + +.sidebar-logo { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.35rem; + margin-bottom: 2.4rem; +} + +.sidebar-logo img { + width: 3.1rem; + height: 3.1rem; + object-fit: contain; +} + +.sidebar-menu { + list-style: none; +} + +.sidebar-menu li { + margin-bottom: 0.8rem; +} + +.sidebar-menu li a, +.logout-btn { + display: flex; + align-items: center; + gap: 0.85rem; + text-decoration: none; + color: #ffffff; + padding: 0.9rem 0.85rem; + border-radius: 0.45rem; + font-size: 0.95rem; + transition: background 0.2s ease; + width: 100%; + border: none; + background: transparent; + cursor: pointer; +} + +.sidebar-menu li a i, +.logout-btn i { + width: 1rem; + text-align: center; +} + +.sidebar-menu li.active a, +.sidebar-menu li a:hover { + background: #1d7de2; +} + +.logout-btn:hover { + background: #ef2f2f; +} + +.main-content { + flex: 1; + display: flex; + flex-direction: column; +} + +.topbar { + height: 5.8rem; + background: #ffffff; + border-bottom: 0.0625rem solid #e6e8eb; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 1.8rem; +} + +.search-box { + width: 21rem; + height: 2.9rem; + display: flex; + align-items: center; + gap: 0.75rem; + border: 0.0625rem solid #d9dde3; + border-radius: 0.35rem; + padding: 0 0.9rem; + background: #ffffff; +} + +.search-box i { + color: #6b7280; + font-size: 0.95rem; +} + +.search-box input { + width: 100%; + border: none; + outline: none; + background: transparent; + font-size: 0.95rem; + color: #374151; +} + +.topbar-right { + display: flex; + align-items: center; + gap: 1rem; +} + +.notification { + position: relative; + color: #111827; + font-size: 1.2rem; +} + +.notification-dot { + position: absolute; + top: 0.08rem; + right: -0.12rem; + width: 0.45rem; + height: 0.45rem; + background: #ef2f2f; + border-radius: 50%; +} + +.admin-profile { + display: flex; + align-items: center; + gap: 0.7rem; +} + +.user-avatar { + width: 2.45rem; + height: 2.45rem; + border-radius: 50%; + object-fit: cover; +} + +.admin-text h4 { + font-size: 0.82rem; + font-weight: 600; + color: #172033; + line-height: 1.2; +} + +.admin-text p { + font-size: 0.72rem; + color: #6b7280; + line-height: 1.2; +} + +.admin-chevron { + color: #6b7280; + font-size: 0.7rem; +} + +.page-content { + padding: 4rem 1.4rem 2rem; +} + +.page-header { + margin-bottom: 2rem; +} + +.page-header h1 { + font-size: 2.1rem; + font-weight: 700; + margin-bottom: 0.3rem; +} + +.page-header p { + font-size: 0.78rem; + color: #6b7280; +} + +.filter-buttons { + display: flex; + gap: 0.8rem; + margin-bottom: 1.4rem; + flex-wrap: wrap; +} + +.filter-btn { + border: 0.0625rem solid #d9dde3; + background: #ffffff; + color: #374151; + padding: 0.75rem 1rem; + border-radius: 0.4rem; + font-size: 0.8rem; + cursor: pointer; +} + +.filter-btn.active { + background: #1d7de2; + color: #ffffff; + border-color: #1d7de2; +} + +.table-wrapper { + background: #ffffff; + overflow: hidden; + border-radius: 0.6rem; +} + +table { + width: 100%; + border-collapse: collapse; +} + +thead { + background: #f4f6f8; +} + +th, +td { + text-align: left; + padding: 1rem 1.4rem; +} + +th { + font-size: 0.62rem; + letter-spacing: 0.08em; + color: #7b8492; + font-weight: 700; +} + +td { + font-size: 0.72rem; + color: #172033; + border-top: 0.0625rem solid #eef1f3; +} + +.status-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 5.5rem; + padding: 0.35rem 0.7rem; + border-radius: 0.45rem; + color: #ffffff; + font-size: 0.58rem; + font-weight: 700; +} + +.status-badge.verified { + background: #16a34a; +} + +.status-badge.rejected { + background: #dc2626; +} + +.status-badge.pending { + background: #f59e0b; +} + +.action-link { + color: #1d7de2; + text-decoration: none; + font-weight: 600; +} + +.action-link:hover { + text-decoration: underline; +} + +.empty-state { + text-align: center; + padding: 3rem 1rem; +} + +.empty-icon { + width: 4rem; + height: 4rem; + margin: 0 auto 1rem; + border-radius: 50%; + background: #e8f0fb; + color: #1d7de2; + display: flex; + align-items: center; + justify-content: center; + position: relative; + font-size: 1.4rem; +} + +.empty-check { + position: absolute; + right: -0.2rem; + bottom: -0.2rem; + font-size: 0.9rem; + color: #16a34a; +} + +.empty-state h2 { + font-size: 1.1rem; + margin-bottom: 0.4rem; +} + +.empty-state p { + color: #6b7280; + font-size: 0.85rem; +} + +.hidden { + display: none !important; +} + +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(15, 23, 42, 0.45); + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; + z-index: 999; +} + +.modal-card { + width: 100%; + max-width: 25rem; + background: #ffffff; + border-radius: 1rem; + padding: 1.5rem; + position: relative; + text-align: center; + box-shadow: 0 1rem 2.5rem rgba(0, 0, 0, 0.15); +} + +.modal-close-btn { + position: absolute; + top: 0.7rem; + right: 0.9rem; + border: none; + background: transparent; + font-size: 1.5rem; + color: #6b7280; + cursor: pointer; +} + +.modal-icon-wrap { + width: 3.5rem; + height: 3.5rem; + margin: 0 auto 1rem; + border-radius: 50%; + background: #fee2e2; + color: #dc2626; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.25rem; +} + +.modal-card h3 { + font-size: 1.2rem; + margin-bottom: 0.5rem; + color: #172033; +} + +.modal-card p { + font-size: 0.92rem; + color: #6b7280; + margin-bottom: 1.3rem; +} + +.modal-actions { + display: flex; + gap: 0.8rem; + justify-content: center; +} + +.modal-btn { + border: none; + border-radius: 0.55rem; + padding: 0.8rem 1rem; + font-size: 0.9rem; + cursor: pointer; + min-width: 8rem; +} + +.modal-btn.secondary { + background: #e5e7eb; + color: #172033; +} + +.modal-btn.danger { + background: #dc2626; + color: #ffffff; +} + +@media (max-width: 75rem) { + .table-wrapper { + overflow-x: auto; + } + + table { + min-width: 46rem; + } +} + +@media (max-width: 62rem) { + .sidebar { + display: none; + } + + .topbar { + padding: 0 1rem; + } + + .search-box { + width: 16rem; + } + + .page-content { + padding: 2rem 1rem; + } + + .admin-text, + .admin-chevron { + display: none; + } +} + +@media (max-width: 36rem) { + .search-box { + width: 12rem; + } + + .modal-actions { + flex-direction: column; + } + + .modal-btn { + width: 100%; + } +} diff --git a/Admin/organizations/organization-directory.html b/Admin/organizations/organization-directory.html new file mode 100644 index 0000000..0a87836 --- /dev/null +++ b/Admin/organizations/organization-directory.html @@ -0,0 +1,153 @@ + + + + + + AIDLoop - Organizer Directory + + + + + + + +
+ + +
+
+ + +
+
+ + +
+ +
+ Admin Avatar +
+

Loading...

+

Admin

+
+ +
+
+
+ +
+ + +
+ + + +
+ +
+ + + + + + + + + + + + + + + + +
ORGANIZATION NAMECONTACT EMAILLOCATIONSTATUSACTIONS
Loading organizations...
+ + +
+
+
+
+ + + + + + diff --git a/Admin/organizations/organization-directory.js b/Admin/organizations/organization-directory.js new file mode 100644 index 0000000..e33acd7 --- /dev/null +++ b/Admin/organizations/organization-directory.js @@ -0,0 +1,260 @@ +const API_BASE_URL = "https://aidloop-backend.onrender.com/api"; + +const els = { + adminName: document.getElementById("adminName"), + adminRole: document.getElementById("adminRole"), + adminAvatar: document.getElementById("adminAvatar"), + directoryTable: document.getElementById("directoryTable"), + directoryTableWrap: document.getElementById("directoryTableWrap"), + emptyState: document.getElementById("emptyState"), + searchInput: document.getElementById("searchInput"), + filterButtons: document.querySelectorAll(".filter-btn"), + logoutBtn: document.getElementById("logoutBtn"), + logoutModal: document.getElementById("logoutModal"), + closeLogoutModal: document.getElementById("closeLogoutModal"), + cancelLogout: document.getElementById("cancelLogout"), + confirmLogout: document.getElementById("confirmLogout") +}; + +let organizersCache = []; +let currentFilter = "all"; + +async function apiRequest(endpoint, options = {}) { + const response = await fetch(`${API_BASE_URL}${endpoint}`, { + credentials: "include", + headers: { + "Content-Type": "application/json", + ...(options.headers || {}) + }, + ...options + }); + + const contentType = response.headers.get("content-type") || ""; + const data = contentType.includes("application/json") + ? await response.json() + : await response.text(); + + if (!response.ok) { + throw new Error( + (data && data.message) || + (data && data.error) || + "Request failed" + ); + } + + return data; +} + +function normalizeUsers(payload) { + if (Array.isArray(payload)) return payload; + if (Array.isArray(payload?.users)) return payload.users; + if (Array.isArray(payload?.data)) return payload.data; + return []; +} + +function getOrganizerStatus(user) { + const status = String(user.status || "").toLowerCase(); + const approvalStatus = String(user.approvalStatus || "").toLowerCase(); + const isVerified = Boolean(user.isVerified); + + if (status === "rejected" || approvalStatus === "rejected") return "rejected"; + + if ( + status === "verified" || + status === "approved" || + approvalStatus === "verified" || + approvalStatus === "approved" || + isVerified + ) { + return "verified"; + } + + return "pending"; +} + +function getDisplayName(user) { + return user.fullName || user.name || user.organizationName || "Unnamed Organizer"; +} + +function getLocation(user) { + if (typeof user.location === "string" && user.location.trim()) { + return user.location; + } + + if (user.location && typeof user.location === "object") { + return ( + [user.location.venue, user.location.city || user.location.state] + .filter(Boolean) + .join(", ") || "—" + ); + } + + return user.city || user.state || "—"; +} + +function renderDirectory() { + const query = els.searchInput.value.trim().toLowerCase(); + + let filtered = [...organizersCache]; + + if (currentFilter !== "all") { + filtered = filtered.filter((organizer) => organizer._status === currentFilter); + } + + if (query) { + filtered = filtered.filter((organizer) => { + const searchableText = ` + ${getDisplayName(organizer)} + ${organizer.email || ""} + ${getLocation(organizer)} + ${organizer._status} + `.toLowerCase(); + + return searchableText.includes(query); + }); + } + + if (!filtered.length) { + els.directoryTableWrap.style.display = "none"; + els.emptyState.style.display = "block"; + return; + } + + els.directoryTableWrap.style.display = "table"; + els.emptyState.style.display = "none"; + + els.directoryTable.innerHTML = filtered.map((organizer) => ` + + ${getDisplayName(organizer)} + ${organizer.email || "—"} + ${getLocation(organizer)} + + + ${organizer._status.charAt(0).toUpperCase() + organizer._status.slice(1)} + + + + + View Details + + + + `).join(""); +} + +function bindFilters() { + els.filterButtons.forEach((button) => { + button.addEventListener("click", () => { + els.filterButtons.forEach((btn) => btn.classList.remove("active")); + button.classList.add("active"); + currentFilter = button.dataset.filter; + renderDirectory(); + }); + }); +} + +function openLogoutModal() { + els.logoutModal.classList.remove("hidden"); +} + +function closeLogoutModal() { + els.logoutModal.classList.add("hidden"); + els.confirmLogout.disabled = false; + els.confirmLogout.textContent = "Yes, Log out"; +} + +async function handleLogout() { + try { + els.confirmLogout.disabled = true; + els.confirmLogout.textContent = "Logging out..."; + + await apiRequest("/auth/logout", { + method: "POST" + }); + } catch (error) { + console.warn("Logout failed:", error.message); + } finally { + localStorage.clear(); + sessionStorage.clear(); + window.location.href = "../../index.html"; + } +} + +async function loadAdminProfile() { + try { + let profile; + try { + profile = await apiRequest("/users/me"); + } catch { + profile = await apiRequest("/user/me"); + } + + els.adminName.textContent = + profile.fullName || + profile.name || + "Admin User"; + + els.adminRole.textContent = + profile.role + ? profile.role.charAt(0).toUpperCase() + profile.role.slice(1) + : "Admin"; + + if (profile.profileImage) { + els.adminAvatar.src = profile.profileImage; + } + } catch (error) { + console.error("Failed to load admin profile:", error.message); + window.location.href = "../profile/admin-profile.html"; + } +} + +async function loadOrganizations() { + try { + const payload = await apiRequest("/user").catch(() => apiRequest("/users")); + const users = normalizeUsers(payload); + + organizersCache = users + .filter((user) => String(user.role || "").toLowerCase() === "organizer") + .map((user) => ({ + ...user, + _status: getOrganizerStatus(user) + })); + + renderDirectory(); + } catch (error) { + console.error("Failed to load organizations:", error.message); + els.directoryTable.innerHTML = ` + + Failed to load organizations. + + `; + } +} + +function bindUI() { + els.searchInput.addEventListener("input", renderDirectory); + bindFilters(); + + els.logoutBtn.addEventListener("click", openLogoutModal); + els.closeLogoutModal.addEventListener("click", closeLogoutModal); + els.cancelLogout.addEventListener("click", closeLogoutModal); + els.confirmLogout.addEventListener("click", handleLogout); + + els.logoutModal.addEventListener("click", (event) => { + if (event.target === els.logoutModal) { + closeLogoutModal(); + } + }); + + document.addEventListener("keydown", (event) => { + if (event.key === "Escape" && !els.logoutModal.classList.contains("hidden")) { + closeLogoutModal(); + } + }); +} + +document.addEventListener("DOMContentLoaded", async () => { + bindUI(); + await loadAdminProfile(); + await loadOrganizations(); +}); \ No newline at end of file diff --git a/Admin/profile/admin-profile.css b/Admin/profile/admin-profile.css new file mode 100644 index 0000000..20c9749 --- /dev/null +++ b/Admin/profile/admin-profile.css @@ -0,0 +1,418 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: "Poppins", sans-serif; +} + +body { + background: #edf2f6; + color: #172033; +} + +.dashboard { + display: flex; + min-height: 100vh; +} + +.sidebar { + width: 15rem; + background: #223f6b; + color: #ffffff; + padding: 1.8rem 0.9rem; +} + +.sidebar-logo { + display: flex; + justify-content: center; + margin-bottom: 2.4rem; +} + +.sidebar-logo img { + width: 3.1rem; + height: 3.1rem; + object-fit: contain; +} + +.sidebar-menu { + list-style: none; +} + +.sidebar-menu li { + margin-bottom: 0.8rem; +} + +.sidebar-menu li a, +.logout-btn { + display: flex; + align-items: center; + gap: 0.85rem; + text-decoration: none; + color: #ffffff; + padding: 0.9rem 0.85rem; + border-radius: 0.45rem; + font-size: 0.95rem; + transition: background 0.2s ease; + width: 100%; + border: none; + background: transparent; + cursor: pointer; +} + +.sidebar-menu li a i, +.logout-btn i { + width: 1rem; + text-align: center; +} + +.sidebar-menu li.active a, +.sidebar-menu li a:hover { + background: #1d7de2; +} + +.logout-btn:hover { + background: #ef2f2f; +} + +.main-content { + flex: 1; + display: flex; + flex-direction: column; +} + +.topbar { + height: 5.8rem; + background: #ffffff; + border-bottom: 0.0625rem solid #e6e8eb; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 1.8rem; +} + +.search-box { + width: 21rem; + height: 2.9rem; + display: flex; + align-items: center; + gap: 0.75rem; + border: 0.0625rem solid #d9dde3; + border-radius: 0.35rem; + padding: 0 0.9rem; + background: #ffffff; +} + +.search-box i { + color: #6b7280; + font-size: 0.95rem; +} + +.search-box input { + width: 100%; + border: none; + outline: none; + background: transparent; + font-size: 0.95rem; + color: #374151; +} + +.topbar-right { + display: flex; + align-items: center; + gap: 1rem; +} + +.notification { + position: relative; + color: #111827; + font-size: 1.2rem; +} + +.notification-dot { + position: absolute; + top: 0.08rem; + right: -0.12rem; + width: 0.45rem; + height: 0.45rem; + background: #ef2f2f; + border-radius: 50%; +} + +.admin-profile-mini { + display: flex; + align-items: center; + gap: 0.7rem; +} + +.user-avatar { + width: 2.45rem; + height: 2.45rem; + border-radius: 50%; + object-fit: cover; +} + +.admin-mini-text h4 { + font-size: 0.82rem; + font-weight: 600; + color: #172033; + line-height: 1.2; +} + +.admin-mini-text p { + font-size: 0.72rem; + color: #6b7280; + line-height: 1.2; +} + +.page-content { + padding: 4rem 1.4rem 2rem; +} + +.page-header { + margin-bottom: 2rem; +} + +.page-header h1 { + font-size: 2.1rem; + font-weight: 700; + margin-bottom: 0.3rem; +} + +.page-header p { + font-size: 0.82rem; + color: #6b7280; +} + +.profile-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1.4rem; +} + +.card { + background: #ffffff; + border-radius: 0.85rem; + padding: 1.4rem; +} + +.card h2 { + font-size: 1.1rem; + margin-bottom: 1rem; + color: #223f6b; +} + +.form { + display: flex; + flex-direction: column; + gap: 0.9rem; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 0.35rem; +} + +.form-group label { + font-size: 0.85rem; + font-weight: 600; + color: #374151; +} + +.form-group input { + width: 100%; + height: 3rem; + border: 0.0625rem solid #d9dde3; + border-radius: 0.5rem; + padding: 0 0.9rem; + font-size: 0.92rem; + color: #172033; + background: #ffffff; + outline: none; +} + +.form-group input[readonly] { + background: #f8fafc; +} + +.button-row { + display: flex; + justify-content: flex-start; + margin-top: 0.3rem; +} + +.button-row.align-right { + justify-content: flex-end; +} + +.primary-btn, +.secondary-btn { + border: none; + border-radius: 0.55rem; + padding: 0.85rem 1.2rem; + font-size: 0.9rem; + cursor: pointer; +} + +.primary-btn { + background: #1d7de2; + color: #ffffff; +} + +.secondary-btn { + background: #223f6b; + color: #ffffff; +} + +.primary-btn:hover, +.secondary-btn:hover { + opacity: 0.94; +} + +.primary-btn:disabled, +.secondary-btn:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +.feedback { + min-height: 1.1rem; + font-size: 0.82rem; +} + +.feedback.success { + color: #16a34a; +} + +.feedback.error { + color: #dc2626; +} + +.hidden { + display: none !important; +} + +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(15, 23, 42, 0.45); + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; + z-index: 999; +} + +.modal-card { + width: 100%; + max-width: 25rem; + background: #ffffff; + border-radius: 1rem; + padding: 1.5rem; + position: relative; + text-align: center; + box-shadow: 0 1rem 2.5rem rgba(0, 0, 0, 0.15); +} + +.modal-close-btn { + position: absolute; + top: 0.7rem; + right: 0.9rem; + border: none; + background: transparent; + font-size: 1.5rem; + color: #6b7280; + cursor: pointer; +} + +.modal-icon-wrap { + width: 3.5rem; + height: 3.5rem; + margin: 0 auto 1rem; + border-radius: 50%; + background: #fee2e2; + color: #dc2626; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.25rem; +} + +.modal-card h3 { + font-size: 1.2rem; + margin-bottom: 0.5rem; + color: #172033; +} + +.modal-card p { + font-size: 0.92rem; + color: #6b7280; + margin-bottom: 1.3rem; +} + +.modal-actions { + display: flex; + gap: 0.8rem; + justify-content: center; +} + +.modal-btn { + border: none; + border-radius: 0.55rem; + padding: 0.8rem 1rem; + font-size: 0.9rem; + cursor: pointer; + min-width: 8rem; +} + +.modal-btn.secondary { + background: #e5e7eb; + color: #172033; +} + +.modal-btn.danger { + background: #dc2626; + color: #ffffff; +} + +@media (max-width: 68rem) { + .profile-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 62rem) { + .sidebar { + display: none; + } + + .topbar { + padding: 0 1rem; + } + + .search-box { + width: 16rem; + } + + .page-content { + padding: 2rem 1rem; + } + + .admin-mini-text { + display: none; + } +} + +@media (max-width: 36rem) { + .search-box { + width: 12rem; + } + + .modal-actions { + flex-direction: column; + } + + .modal-btn { + width: 100%; + } +} \ No newline at end of file diff --git a/Admin/profile/admin-profile.html b/Admin/profile/admin-profile.html new file mode 100644 index 0000000..5e9f539 --- /dev/null +++ b/Admin/profile/admin-profile.html @@ -0,0 +1,174 @@ + + + + + + AIDLoop - Admin Profile + + + + + + + + +
+ + +
+
+ + +
+
+ + +
+ +
+ Admin Avatar +
+

Loading...

+

Admin

+
+
+
+
+ +
+ + +
+
+

Profile Information

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + +
+ +
+
+
+ +
+

Security

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ + + +
+ +
+
+
+
+
+
+
+ + + + + + \ No newline at end of file diff --git a/Admin/profile/admin-profile.js b/Admin/profile/admin-profile.js new file mode 100644 index 0000000..7abe65a --- /dev/null +++ b/Admin/profile/admin-profile.js @@ -0,0 +1,220 @@ +const API_BASE_URL = "https://aidloop-backend.onrender.com/api"; + +const els = { + adminAvatar: document.getElementById("adminAvatar"), + adminNameMini: document.getElementById("adminNameMini"), + adminRoleMini: document.getElementById("adminRoleMini"), + fullName: document.getElementById("fullName"), + emailAddress: document.getElementById("emailAddress"), + role: document.getElementById("role"), + phoneNumber: document.getElementById("phoneNumber"), + editProfileBtn: document.getElementById("editProfileBtn"), + profileFeedback: document.getElementById("profileFeedback"), + currentPassword: document.getElementById("currentPassword"), + newPassword: document.getElementById("newPassword"), + confirmPassword: document.getElementById("confirmPassword"), + passwordForm: document.getElementById("passwordForm"), + passwordFeedback: document.getElementById("passwordFeedback"), + logoutBtn: document.getElementById("logoutBtn"), + logoutModal: document.getElementById("logoutModal"), + closeLogoutModal: document.getElementById("closeLogoutModal"), + cancelLogout: document.getElementById("cancelLogout"), + confirmLogout: document.getElementById("confirmLogout") +}; + +let profileEditMode = false; +let currentAdmin = null; + +async function apiRequest(endpoint, options = {}) { + const response = await fetch(`${API_BASE_URL}${endpoint}`, { + credentials: "include", + headers: { + "Content-Type": "application/json", + ...(options.headers || {}) + }, + ...options + }); + + const contentType = response.headers.get("content-type") || ""; + const data = contentType.includes("application/json") + ? await response.json() + : await response.text(); + + if (!response.ok) { + throw new Error( + (data && data.message) || + (data && data.error) || + "Request failed" + ); + } + + return data; +} + +function setFeedback(element, message, type = "") { + element.textContent = message; + element.className = "feedback"; + if (type) { + element.classList.add(type); + } +} + +function fillProfile(profile) { + currentAdmin = profile; + + const fullName = profile.fullName || profile.name || "Admin User"; + const role = profile.role + ? profile.role.charAt(0).toUpperCase() + profile.role.slice(1) + : "Admin"; + + els.adminNameMini.textContent = fullName; + els.adminRoleMini.textContent = role; + + els.fullName.value = fullName; + els.emailAddress.value = profile.email || ""; + els.role.value = role; + els.phoneNumber.value = profile.phoneNumber || profile.phone || ""; + + if (profile.profileImage) { + els.adminAvatar.src = profile.profileImage; + } +} + +function setProfileInputsReadonly(readonly) { + els.phoneNumber.readOnly = readonly; +} + +function toggleProfileEditMode(forceValue = null) { + profileEditMode = forceValue !== null ? forceValue : !profileEditMode; + setProfileInputsReadonly(!profileEditMode); + els.editProfileBtn.textContent = profileEditMode ? "Save Profile" : "Edit Profile"; +} + +async function loadAdminProfile() { + try { + let profile; + try { + profile = await apiRequest("/users/me"); + } catch { + profile = await apiRequest("/user/me"); + } + + fillProfile(profile); + toggleProfileEditMode(false); + } catch (error) { + setFeedback(els.profileFeedback, error.message || "Failed to load profile.", "error"); + } +} + +async function saveProfile() { + try { + els.editProfileBtn.disabled = true; + + const updated = await apiRequest("/user/me", { + method: "PUT", + body: JSON.stringify({ + phoneNumber: els.phoneNumber.value.trim() + }) + }); + + fillProfile({ + ...currentAdmin, + ...updated, + phoneNumber: updated.phoneNumber || els.phoneNumber.value.trim() + }); + + toggleProfileEditMode(false); + setFeedback(els.profileFeedback, "Profile updated successfully.", "success"); + } catch (error) { + setFeedback(els.profileFeedback, error.message || "Failed to update profile.", "error"); + } finally { + els.editProfileBtn.disabled = false; + } +} + +function handleEditProfile() { + setFeedback(els.profileFeedback, ""); + if (!profileEditMode) { + toggleProfileEditMode(true); + return; + } + saveProfile(); +} + +function handlePasswordSubmit(event) { + event.preventDefault(); + + const currentPassword = els.currentPassword.value.trim(); + const newPassword = els.newPassword.value.trim(); + const confirmPassword = els.confirmPassword.value.trim(); + + if (!currentPassword || !newPassword || !confirmPassword) { + setFeedback(els.passwordFeedback, "All password fields are required.", "error"); + return; + } + + if (newPassword !== confirmPassword) { + setFeedback(els.passwordFeedback, "New passwords do not match.", "error"); + return; + } + + setFeedback( + els.passwordFeedback, + "No admin change-password endpoint has been provided yet.", + "error" + ); +} + +function openLogoutModal() { + els.logoutModal.classList.remove("hidden"); +} + +function closeLogoutModal() { + els.logoutModal.classList.add("hidden"); + els.confirmLogout.disabled = false; + els.confirmLogout.textContent = "Yes, Log out"; +} + +async function handleLogout() { + try { + els.confirmLogout.disabled = true; + els.confirmLogout.textContent = "Logging out..."; + + await apiRequest("/auth/logout", { + method: "POST" + }); + } catch (error) { + console.warn("Logout failed:", error.message); + } finally { + localStorage.clear(); + sessionStorage.clear(); + window.location.href = "../../index.html"; + } +} + +function bindUI() { + els.editProfileBtn.addEventListener("click", handleEditProfile); + els.passwordForm.addEventListener("submit", handlePasswordSubmit); + + els.logoutBtn.addEventListener("click", openLogoutModal); + els.closeLogoutModal.addEventListener("click", closeLogoutModal); + els.cancelLogout.addEventListener("click", closeLogoutModal); + els.confirmLogout.addEventListener("click", handleLogout); + + els.logoutModal.addEventListener("click", (event) => { + if (event.target === els.logoutModal) { + closeLogoutModal(); + } + }); + + document.addEventListener("keydown", (event) => { + if (event.key === "Escape" && !els.logoutModal.classList.contains("hidden")) { + closeLogoutModal(); + } + }); +} + +document.addEventListener("DOMContentLoaded", async () => { + bindUI(); + await loadAdminProfile(); +}); \ No newline at end of file diff --git a/Admin/user/user-details.css b/Admin/user/user-details.css new file mode 100644 index 0000000..502e744 --- /dev/null +++ b/Admin/user/user-details.css @@ -0,0 +1,201 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: "Poppins", sans-serif; +} + +body { + background: #edf2f6; + color: #172033; +} + +.page { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 1.5rem; +} + +.details-card { + width: 100%; + max-width: 42rem; + background: #ffffff; + border-radius: 1rem; + padding: 1.8rem; + box-shadow: 0 1rem 2.5rem rgba(0, 0, 0, 0.12); +} + +.header-row { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1.5rem; +} + +.header-row h1 { + font-size: 1.6rem; + font-weight: 700; + color: #172033; + line-height: 1.3; +} + +.role-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 6rem; + padding: 0.45rem 0.85rem; + border-radius: 0.55rem; + color: #ffffff; + font-size: 0.72rem; + font-weight: 700; + text-transform: capitalize; +} + +.role-badge.organizer { + background: #1d7de2; +} + +.role-badge.volunteer { + background: #16a34a; +} + +.role-badge.admin { + background: #6b7280; +} + +.role-badge.user { + background: #64748b; +} + +.info-grid { + display: grid; + grid-template-columns: 8rem 1fr; + gap: 0.9rem 1rem; + margin-bottom: 1.5rem; +} + +.label { + font-size: 0.9rem; + font-weight: 600; + color: #223f6b; +} + +.value { + font-size: 0.9rem; + color: #374151; + word-break: break-word; +} + +.status-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 6rem; + padding: 0.4rem 0.8rem; + border-radius: 0.5rem; + color: #ffffff; + font-size: 0.72rem; + font-weight: 700; +} + +.status-badge.active { + background: #16a34a; +} + +.status-badge.deactivated { + background: #dc2626; +} + +.description-section h2 { + font-size: 1rem; + font-weight: 700; + margin-bottom: 0.7rem; + color: #172033; +} + +.description-box { + background: #f8fafc; + border: 0.0625rem solid #e5e7eb; + border-radius: 0.75rem; + padding: 1rem; + font-size: 0.9rem; + line-height: 1.7; + color: #374151; +} + +.feedback { + min-height: 1.2rem; + margin-top: 1rem; + text-align: center; + font-size: 0.85rem; +} + +.feedback.success { + color: #16a34a; +} + +.feedback.error { + color: #dc2626; +} + +.action-row { + margin-top: 1.3rem; + display: flex; + justify-content: flex-end; + gap: 0.8rem; +} + +.action-btn { + border: none; + border-radius: 0.55rem; + padding: 0.85rem 1.2rem; + font-size: 0.9rem; + cursor: pointer; +} + +.deactivate-btn { + background: #dc2626; + color: #ffffff; +} + +.reactivate-btn { + background: #e5e7eb; + color: #172033; +} + +.action-btn:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +@media (max-width: 36rem) { + .details-card { + padding: 1.2rem; + } + + .header-row { + flex-direction: column; + align-items: flex-start; + } + + .info-grid { + grid-template-columns: 1fr; + gap: 0.4rem; + } + + .label { + margin-top: 0.4rem; + } + + .action-row { + flex-direction: column; + } + + .action-btn { + width: 100%; + } +} \ No newline at end of file diff --git a/Admin/user/user-details.html b/Admin/user/user-details.html new file mode 100644 index 0000000..827d916 --- /dev/null +++ b/Admin/user/user-details.html @@ -0,0 +1,61 @@ + + + + + + User Details + + + + + + + +
+
+
+

Loading...

+ User +
+ +
+
Name:
+
Loading...
+ +
Email:
+
+ +
Phone No:
+
+ +
Location:
+
+ +
Date Joined:
+
+ +
Status
+
+ Loading... +
+
+ +
+

Description

+
+ Loading description... +
+
+ + + +
+ + +
+
+
+ + + + \ No newline at end of file diff --git a/Admin/user/user-details.js b/Admin/user/user-details.js new file mode 100644 index 0000000..02219db --- /dev/null +++ b/Admin/user/user-details.js @@ -0,0 +1,214 @@ +const API_BASE_URL = "https://aidloop-backend.onrender.com/api"; + +const els = { + userTitle: document.getElementById("userTitle"), + roleBadge: document.getElementById("roleBadge"), + userName: document.getElementById("userName"), + email: document.getElementById("email"), + phoneNumber: document.getElementById("phoneNumber"), + location: document.getElementById("location"), + dateJoined: document.getElementById("dateJoined"), + statusBadge: document.getElementById("statusBadge"), + description: document.getElementById("description"), + feedback: document.getElementById("feedback"), + deactivateBtn: document.getElementById("deactivateBtn"), + reactivateBtn: document.getElementById("reactivateBtn") +}; + +const userId = new URLSearchParams(window.location.search).get("id"); +let currentUser = null; + +async function apiRequest(endpoint, options = {}) { + const response = await fetch(`${API_BASE_URL}${endpoint}`, { + credentials: "include", + headers: { + "Content-Type": "application/json", + ...(options.headers || {}) + }, + ...options + }); + + const contentType = response.headers.get("content-type") || ""; + const data = contentType.includes("application/json") + ? await response.json() + : await response.text(); + + if (!response.ok) { + throw new Error( + (data && data.message) || + (data && data.error) || + "Request failed" + ); + } + + return data; +} + +function normalizeUsers(payload) { + if (Array.isArray(payload)) return payload; + if (Array.isArray(payload?.users)) return payload.users; + if (Array.isArray(payload?.data)) return payload.data; + return []; +} + +function formatDate(dateValue) { + if (!dateValue) return "—"; + const date = new Date(dateValue); + if (Number.isNaN(date.getTime())) return dateValue; + return date.toLocaleDateString("en-GB", { + day: "2-digit", + month: "long", + year: "numeric" + }); +} + +function getDisplayName(user) { + return user.fullName || user.name || user.organizationName || "User"; +} + +function getLocation(user) { + if (typeof user.location === "string" && user.location.trim()) { + return user.location; + } + + if (user.location && typeof user.location === "object") { + return ( + [user.location.venue, user.location.city || user.location.state] + .filter(Boolean) + .join(", ") || "—" + ); + } + + return user.city || user.state || "—"; +} + +function getRole(user) { + return String(user.role || "user").toLowerCase(); +} + +function getStatus(user) { + return user.isActive === false ? "deactivated" : "active"; +} + +function setFeedback(message, type = "") { + els.feedback.textContent = message; + els.feedback.className = "feedback"; + if (type) { + els.feedback.classList.add(type); + } +} + +function setRoleBadge(role) { + els.roleBadge.textContent = role.charAt(0).toUpperCase() + role.slice(1); + els.roleBadge.className = "role-badge"; + els.roleBadge.classList.add(role); +} + +function setStatusBadge(status) { + els.statusBadge.textContent = + status === "deactivated" ? "Deactivated" : "Active"; + els.statusBadge.className = "status-badge"; + els.statusBadge.classList.add(status); +} + +function syncButtons(status) { + if (status === "deactivated") { + els.deactivateBtn.disabled = true; + els.deactivateBtn.textContent = "Deactivated"; + els.reactivateBtn.disabled = false; + } else { + els.deactivateBtn.disabled = false; + els.deactivateBtn.textContent = "Deactivate"; + els.reactivateBtn.disabled = false; + } +} + +function populateUser(user) { + currentUser = user; + + const role = getRole(user); + const status = getStatus(user); + + els.userTitle.textContent = getDisplayName(user); + els.userName.textContent = getDisplayName(user); + els.email.textContent = user.email || "—"; + els.phoneNumber.textContent = user.phoneNumber || user.phone || "—"; + els.location.textContent = getLocation(user); + els.dateJoined.textContent = formatDate(user.createdAt || user.dateJoined); + els.description.textContent = + user.description || + user.bio || + "No description available."; + + setRoleBadge(role); + setStatusBadge(status); + syncButtons(status); +} + +async function loadUserDetails() { + if (!userId) { + els.userTitle.textContent = "No user selected"; + els.description.textContent = "No user ID provided."; + els.deactivateBtn.disabled = true; + els.reactivateBtn.disabled = true; + return; + } + + try { + const payload = await apiRequest("/user").catch(() => apiRequest("/users")); + const users = normalizeUsers(payload); + + const user = users.find( + (item) => String(item._id || item.id) === String(userId) + ); + + if (!user) { + throw new Error("User not found"); + } + + populateUser(user); + } catch (error) { + els.userTitle.textContent = "Unable to load user"; + els.description.textContent = error.message || "Failed to fetch user details."; + els.deactivateBtn.disabled = true; + els.reactivateBtn.disabled = true; + setFeedback(error.message || "Failed to load user details.", "error"); + } +} + +async function deactivateUser() { + if (!userId) return; + + try { + els.deactivateBtn.disabled = true; + els.deactivateBtn.textContent = "Deactivating..."; + + await apiRequest(`/admin/users/${userId}/deactivate`, { + method: "PATCH" + }); + + currentUser = { + ...currentUser, + isActive: false + }; + + setStatusBadge("deactivated"); + syncButtons("deactivated"); + setFeedback("User deactivated successfully.", "success"); + } catch (error) { + syncButtons(getStatus(currentUser || {})); + setFeedback(error.message || "Failed to deactivate user.", "error"); + } +} + +function handleReactivate() { + setFeedback( + "No reactivation endpoint has been provided yet.", + "error" + ); +} + +els.deactivateBtn.addEventListener("click", deactivateUser); +els.reactivateBtn.addEventListener("click", handleReactivate); + +document.addEventListener("DOMContentLoaded", loadUserDetails); \ No newline at end of file diff --git a/Admin/user/user-management.css b/Admin/user/user-management.css new file mode 100644 index 0000000..bbdd83e --- /dev/null +++ b/Admin/user/user-management.css @@ -0,0 +1,434 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: "Poppins", sans-serif; +} + +body { + background: #edf2f6; + color: #172033; +} + +.dashboard { + display: flex; + min-height: 100vh; +} + +.sidebar { + width: 15rem; + background: #223f6b; + color: #ffffff; + padding: 1.8rem 0.9rem; +} + +.sidebar-logo { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.35rem; + margin-bottom: 2.4rem; +} + +.sidebar-logo img { + width: 3.1rem; + height: 3.1rem; + object-fit: contain; +} + +.sidebar-menu { + list-style: none; +} + +.sidebar-menu li { + margin-bottom: 0.8rem; +} + +.sidebar-menu li a, +.logout-btn { + display: flex; + align-items: center; + gap: 0.85rem; + text-decoration: none; + color: #ffffff; + padding: 0.9rem 0.85rem; + border-radius: 0.45rem; + font-size: 0.95rem; + transition: background 0.2s ease; + width: 100%; + border: none; + background: transparent; + cursor: pointer; +} + +.sidebar-menu li a i, +.logout-btn i { + width: 1rem; + text-align: center; +} + +.sidebar-menu li.active a, +.sidebar-menu li a:hover { + background: #1d7de2; +} + +.logout-btn:hover { + background: #ef2f2f; +} + +.main-content { + flex: 1; + display: flex; + flex-direction: column; +} + +.topbar { + height: 5.8rem; + background: #ffffff; + border-bottom: 0.0625rem solid #e6e8eb; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 1.8rem; +} + +.search-box { + width: 21rem; + height: 2.9rem; + display: flex; + align-items: center; + gap: 0.75rem; + border: 0.0625rem solid #d9dde3; + border-radius: 0.35rem; + padding: 0 0.9rem; + background: #ffffff; +} + +.search-box i { + color: #6b7280; + font-size: 0.95rem; +} + +.search-box input { + width: 100%; + border: none; + outline: none; + background: transparent; + font-size: 0.95rem; + color: #374151; +} + +.topbar-right { + display: flex; + align-items: center; + gap: 1rem; +} + +.notification { + position: relative; + color: #111827; + font-size: 1.2rem; +} + +.notification-dot { + position: absolute; + top: 0.08rem; + right: -0.12rem; + width: 0.45rem; + height: 0.45rem; + background: #ef2f2f; + border-radius: 50%; +} + +.admin-profile { + display: flex; + align-items: center; + gap: 0.7rem; +} + +.user-avatar { + width: 2.45rem; + height: 2.45rem; + border-radius: 50%; + object-fit: cover; +} + +.admin-text h4 { + font-size: 0.82rem; + font-weight: 600; + color: #172033; + line-height: 1.2; +} + +.admin-text p { + font-size: 0.72rem; + color: #6b7280; + line-height: 1.2; +} + +.page-content { + padding: 4rem 1.4rem 2rem; +} + +.page-header { + margin-bottom: 2rem; +} + +.page-header h1 { + font-size: 2.1rem; + font-weight: 700; + margin-bottom: 0.3rem; +} + +.page-header p { + font-size: 0.78rem; + color: #6b7280; +} + +.table-wrapper { + background: #ffffff; + overflow: hidden; + border-radius: 0.6rem; +} + +table { + width: 100%; + border-collapse: collapse; +} + +thead { + background: #f4f6f8; +} + +th, +td { + text-align: left; + padding: 1rem 1.4rem; +} + +th { + font-size: 0.62rem; + letter-spacing: 0.08em; + color: #7b8492; + font-weight: 700; +} + +td { + font-size: 0.72rem; + color: #172033; + border-top: 0.0625rem solid #eef1f3; +} + +.role-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 5.5rem; + padding: 0.35rem 0.7rem; + border-radius: 0.45rem; + color: #ffffff; + font-size: 0.58rem; + font-weight: 700; + text-transform: capitalize; +} + +.role-badge.organizer { + background: #1d7de2; +} + +.role-badge.volunteer { + background: #16a34a; +} + +.role-badge.admin { + background: #6b7280; +} + +.actions-cell { + display: flex; + gap: 0.6rem; + flex-wrap: wrap; +} + +.action-link, +.action-btn { + color: #1d7de2; + text-decoration: none; + font-weight: 600; + font-size: 0.72rem; + border: none; + background: transparent; + cursor: pointer; +} + +.action-link:hover, +.action-btn:hover { + text-decoration: underline; +} + +.action-btn.deactivated { + color: #9ca3af; + cursor: not-allowed; + text-decoration: none; +} + +.empty-state { + text-align: center; + padding: 3rem 1rem; +} + +.empty-icon { + width: 4rem; + height: 4rem; + margin: 0 auto 1rem; + border-radius: 50%; + background: #e8f0fb; + color: #1d7de2; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.4rem; +} + +.empty-state h2 { + font-size: 1.1rem; + margin-bottom: 0.4rem; +} + +.empty-state p { + color: #6b7280; + font-size: 0.85rem; +} + +.hidden { + display: none !important; +} + +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(15, 23, 42, 0.45); + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; + z-index: 999; +} + +.modal-card { + width: 100%; + max-width: 25rem; + background: #ffffff; + border-radius: 1rem; + padding: 1.5rem; + position: relative; + text-align: center; + box-shadow: 0 1rem 2.5rem rgba(0, 0, 0, 0.15); +} + +.modal-close-btn { + position: absolute; + top: 0.7rem; + right: 0.9rem; + border: none; + background: transparent; + font-size: 1.5rem; + color: #6b7280; + cursor: pointer; +} + +.modal-icon-wrap { + width: 3.5rem; + height: 3.5rem; + margin: 0 auto 1rem; + border-radius: 50%; + background: #fee2e2; + color: #dc2626; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.25rem; +} + +.modal-card h3 { + font-size: 1.2rem; + margin-bottom: 0.5rem; + color: #172033; +} + +.modal-card p { + font-size: 0.92rem; + color: #6b7280; + margin-bottom: 1.3rem; +} + +.modal-actions { + display: flex; + gap: 0.8rem; + justify-content: center; +} + +.modal-btn { + border: none; + border-radius: 0.55rem; + padding: 0.8rem 1rem; + font-size: 0.9rem; + cursor: pointer; + min-width: 8rem; +} + +.modal-btn.secondary { + background: #e5e7eb; + color: #172033; +} + +.modal-btn.danger { + background: #dc2626; + color: #ffffff; +} + +@media (max-width: 75rem) { + .table-wrapper { + overflow-x: auto; + } + + table { + min-width: 48rem; + } +} + +@media (max-width: 62rem) { + .sidebar { + display: none; + } + + .topbar { + padding: 0 1rem; + } + + .search-box { + width: 16rem; + } + + .page-content { + padding: 2rem 1rem; + } + + .admin-text { + display: none; + } +} + +@media (max-width: 36rem) { + .search-box { + width: 12rem; + } + + .modal-actions { + flex-direction: column; + } + + .modal-btn { + width: 100%; + } +} \ No newline at end of file diff --git a/Admin/user/user-management.html b/Admin/user/user-management.html new file mode 100644 index 0000000..833bd89 --- /dev/null +++ b/Admin/user/user-management.html @@ -0,0 +1,141 @@ + + + + + + AIDLoop - User Management + + + + + + + + +
+ + +
+
+ + +
+
+ + +
+ +
+ Admin Avatar +
+

Loading...

+

Admin

+
+
+
+
+ +
+ + +
+ + + + + + + + + + + + + + + + +
USER NAMEUSER EMAILLOCATIONROLEACTIONS
Loading users...
+ + +
+
+
+
+ + + + + + diff --git a/Admin/user/user-management.js b/Admin/user/user-management.js new file mode 100644 index 0000000..0336af8 --- /dev/null +++ b/Admin/user/user-management.js @@ -0,0 +1,255 @@ +const API_BASE_URL = "https://aidloop-backend.onrender.com/api"; + +const els = { + adminName: document.getElementById("adminName"), + adminRole: document.getElementById("adminRole"), + adminAvatar: document.getElementById("adminAvatar"), + userTable: document.getElementById("userTable"), + userTableWrap: document.getElementById("userTableWrap"), + emptyState: document.getElementById("emptyState"), + searchInput: document.getElementById("searchInput"), + logoutBtn: document.getElementById("logoutBtn"), + logoutModal: document.getElementById("logoutModal"), + closeLogoutModal: document.getElementById("closeLogoutModal"), + cancelLogout: document.getElementById("cancelLogout"), + confirmLogout: document.getElementById("confirmLogout") +}; + +let usersCache = []; + +async function apiRequest(endpoint, options = {}) { + const response = await fetch(`${API_BASE_URL}${endpoint}`, { + credentials: "include", + headers: { + "Content-Type": "application/json", + ...(options.headers || {}) + }, + ...options + }); + + const contentType = response.headers.get("content-type") || ""; + const data = contentType.includes("application/json") + ? await response.json() + : await response.text(); + + if (!response.ok) { + throw new Error( + (data && data.message) || + (data && data.error) || + "Request failed" + ); + } + + return data; +} + +function normalizeUsers(payload) { + if (Array.isArray(payload)) return payload; + if (Array.isArray(payload?.users)) return payload.users; + if (Array.isArray(payload?.data)) return payload.data; + return []; +} + +function getDisplayName(user) { + return user.fullName || user.name || user.organizationName || "User"; +} + +function getLocation(user) { + if (typeof user.location === "string" && user.location.trim()) { + return user.location; + } + + if (user.location && typeof user.location === "object") { + return ( + [user.location.venue, user.location.city || user.location.state] + .filter(Boolean) + .join(", ") || "—" + ); + } + + return user.city || user.state || "—"; +} + +function getRole(user) { + return String(user.role || "user").toLowerCase(); +} + +function renderUsers() { + const query = els.searchInput.value.trim().toLowerCase(); + + const filtered = usersCache.filter((user) => { + const searchableText = ` + ${getDisplayName(user)} + ${user.email || ""} + ${getLocation(user)} + ${getRole(user)} + `.toLowerCase(); + + return searchableText.includes(query); + }); + + if (!filtered.length) { + els.userTableWrap.style.display = "none"; + els.emptyState.style.display = "block"; + return; + } + + els.userTableWrap.style.display = "table"; + els.emptyState.style.display = "none"; + + els.userTable.innerHTML = filtered.map((user) => { + const role = getRole(user); + const id = user._id || user.id || ""; + const isActive = user.isActive !== false; + + return ` + + ${getDisplayName(user)} + ${user.email || "—"} + ${getLocation(user)} + ${role} + +
+ View + +
+ + + `; + }).join(""); + + bindDeactivateButtons(); +} + +function bindDeactivateButtons() { + document.querySelectorAll(".action-btn[data-id]").forEach((button) => { + button.addEventListener("click", async () => { + const id = button.dataset.id; + if (!id || button.disabled) return; + + try { + button.disabled = true; + button.textContent = "Deactivating..."; + + await apiRequest(`/admin/users/${id}/deactivate`, { + method: "PATCH" + }); + + usersCache = usersCache.map((user) => + String(user._id || user.id) === String(id) + ? { ...user, isActive: false } + : user + ); + + renderUsers(); + } catch (error) { + console.error("Failed to deactivate user:", error.message); + button.disabled = false; + button.textContent = "Deactivate"; + } + }); + }); +} + +function openLogoutModal() { + els.logoutModal.classList.remove("hidden"); +} + +function closeLogoutModal() { + els.logoutModal.classList.add("hidden"); + els.confirmLogout.disabled = false; + els.confirmLogout.textContent = "Yes, Log out"; +} + +async function handleLogout() { + try { + els.confirmLogout.disabled = true; + els.confirmLogout.textContent = "Logging out..."; + + await apiRequest("/auth/logout", { + method: "POST" + }); + } catch (error) { + console.warn("Logout failed:", error.message); + } finally { + localStorage.clear(); + sessionStorage.clear(); + window.location.href = "../../index.html"; + } +} + +async function loadAdminProfile() { + try { + let profile; + try { + profile = await apiRequest("/users/me"); + } catch { + profile = await apiRequest("/user/me"); + } + + els.adminName.textContent = + profile.fullName || + profile.name || + "Admin User"; + + els.adminRole.textContent = + profile.role + ? profile.role.charAt(0).toUpperCase() + profile.role.slice(1) + : "Admin"; + + if (profile.profileImage) { + els.adminAvatar.src = profile.profileImage; + } + } catch (error) { + console.error("Failed to load admin profile:", error.message); + window.location.href = "../profile/admin-profile.html"; + } +} + +async function loadUsers() { + try { + const payload = await apiRequest("/user").catch(() => apiRequest("/users")); + usersCache = normalizeUsers(payload); + renderUsers(); + } catch (error) { + console.error("Failed to load users:", error.message); + els.userTable.innerHTML = ` + + Failed to load users. + + `; + } +} + +function bindUI() { + els.searchInput.addEventListener("input", renderUsers); + + els.logoutBtn.addEventListener("click", openLogoutModal); + els.closeLogoutModal.addEventListener("click", closeLogoutModal); + els.cancelLogout.addEventListener("click", closeLogoutModal); + els.confirmLogout.addEventListener("click", handleLogout); + + els.logoutModal.addEventListener("click", (event) => { + if (event.target === els.logoutModal) { + closeLogoutModal(); + } + }); + + document.addEventListener("keydown", (event) => { + if (event.key === "Escape" && !els.logoutModal.classList.contains("hidden")) { + closeLogoutModal(); + } + }); +} + +document.addEventListener("DOMContentLoaded", async () => { + bindUI(); + await loadAdminProfile(); + await loadUsers(); +}); \ No newline at end of file diff --git a/Admin/verification/verification-details.css b/Admin/verification/verification-details.css new file mode 100644 index 0000000..26d62ec --- /dev/null +++ b/Admin/verification/verification-details.css @@ -0,0 +1,186 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: "Poppins", sans-serif; +} + +body { + background: #edf2f6; + color: #172033; +} + +.page { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 1.5rem; +} + +.card { + width: 100%; + max-width: 42rem; + background: #ffffff; + border-radius: 1rem; + padding: 1.8rem; + box-shadow: 0 1rem 2.5rem rgba(0, 0, 0, 0.12); +} + +.card-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1.5rem; +} + +.card-header h1 { + font-size: 1.6rem; + font-weight: 700; + color: #172033; + line-height: 1.3; +} + +.status-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 7rem; + padding: 0.45rem 0.85rem; + border-radius: 0.55rem; + color: #ffffff; + font-size: 0.72rem; + font-weight: 700; + text-transform: capitalize; +} + +.status-badge.awaiting { + background: #f59e0b; +} + +.status-badge.verified { + background: #16a34a; +} + +.status-badge.rejected { + background: #dc2626; +} + +.details-grid { + display: grid; + grid-template-columns: 8rem 1fr; + gap: 0.9rem 1rem; + margin-bottom: 1.5rem; +} + +.label { + font-size: 0.9rem; + font-weight: 600; + color: #223f6b; +} + +.value { + font-size: 0.9rem; + color: #374151; + word-break: break-word; +} + +.description-section h2 { + font-size: 1rem; + font-weight: 700; + margin-bottom: 0.7rem; + color: #172033; +} + +.description-box { + background: #f8fafc; + border: 0.0625rem solid #e5e7eb; + border-radius: 0.75rem; + padding: 1rem; + font-size: 0.9rem; + line-height: 1.7; + color: #374151; +} + +.actions { + margin-top: 1.3rem; + display: flex; + justify-content: flex-end; + gap: 0.8rem; +} + +.btn { + border: none; + border-radius: 0.55rem; + padding: 0.85rem 1.2rem; + font-size: 0.9rem; + cursor: pointer; +} + +.btn-reject { + background: #dc2626; + color: #ffffff; +} + +.btn-approve { + background: #16a34a; + color: #ffffff; +} + +.btn:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +.feedback { + min-height: 1.2rem; + margin-top: 1rem; + text-align: center; + font-size: 0.85rem; +} + +.feedback.success { + color: #16a34a; +} + +.feedback.error { + color: #dc2626; +} + +.social-link { + color: #1d7de2; + text-decoration: none; +} + +.social-link:hover { + text-decoration: underline; +} + +@media (max-width: 36rem) { + .card { + padding: 1.2rem; + } + + .card-header { + flex-direction: column; + align-items: flex-start; + } + + .details-grid { + grid-template-columns: 1fr; + gap: 0.4rem; + } + + .label { + margin-top: 0.4rem; + } + + .actions { + flex-direction: column; + } + + .btn { + width: 100%; + } +} \ No newline at end of file diff --git a/Admin/verification/verification-details.html b/Admin/verification/verification-details.html new file mode 100644 index 0000000..b7aff3a --- /dev/null +++ b/Admin/verification/verification-details.html @@ -0,0 +1,56 @@ + + + + + + Verification Details + + + + + + + +
+
+
+

Loading...

+ Awaiting Verification +
+ +
+
Org. Name:
+
Loading...
+ +
Social Links:
+ + +
Email:
+
+ +
Phone No:
+
+ +
Location:
+
+
+ +
+

Description

+
+

Loading organization description...

+
+
+ +
+ + +
+ + +
+
+ + + + \ No newline at end of file diff --git a/Admin/verification/verification-details.js b/Admin/verification/verification-details.js new file mode 100644 index 0000000..d7f74ed --- /dev/null +++ b/Admin/verification/verification-details.js @@ -0,0 +1,248 @@ +const API_BASE_URL = "https://aidloop-backend.onrender.com/api"; + +const els = { + orgTitle: document.getElementById("orgTitle"), + statusBadge: document.getElementById("statusBadge"), + orgName: document.getElementById("orgName"), + socialLinks: document.getElementById("socialLinks"), + email: document.getElementById("email"), + phoneNumber: document.getElementById("phoneNumber"), + location: document.getElementById("location"), + description: document.getElementById("description"), + rejectBtn: document.getElementById("rejectBtn"), + approveBtn: document.getElementById("approveBtn"), + feedback: document.getElementById("feedback") +}; + +const organizerId = new URLSearchParams(window.location.search).get("id"); +let currentOrganizer = null; + +async function apiRequest(endpoint, options = {}) { + const response = await fetch(`${API_BASE_URL}${endpoint}`, { + credentials: "include", + headers: { + "Content-Type": "application/json", + ...(options.headers || {}) + }, + ...options + }); + + const contentType = response.headers.get("content-type") || ""; + const data = contentType.includes("application/json") + ? await response.json() + : await response.text(); + + if (!response.ok) { + throw new Error( + (data && data.message) || + (data && data.error) || + "Request failed" + ); + } + + return data; +} + +function normalizeUsers(payload) { + if (Array.isArray(payload)) return payload; + if (Array.isArray(payload?.users)) return payload.users; + if (Array.isArray(payload?.data)) return payload.data; + return []; +} + +function getDisplayName(user) { + return user.fullName || user.name || user.organizationName || "Organization"; +} + +function getLocation(user) { + if (typeof user.location === "string" && user.location.trim()) { + return user.location; + } + + if (user.location && typeof user.location === "object") { + return ( + [user.location.venue, user.location.city || user.location.state] + .filter(Boolean) + .join(", ") || "—" + ); + } + + return user.city || user.state || "—"; +} + +function getVerificationStatus(user) { + const status = String(user.status || "").toLowerCase(); + const approvalStatus = String(user.approvalStatus || "").toLowerCase(); + const isVerified = Boolean(user.isVerified); + + if (status === "rejected" || approvalStatus === "rejected") return "rejected"; + + if ( + status === "verified" || + status === "approved" || + approvalStatus === "verified" || + approvalStatus === "approved" || + isVerified + ) { + return "verified"; + } + + return "awaiting"; +} + +function setStatusBadge(status) { + els.statusBadge.textContent = + status === "verified" + ? "Verified" + : status === "rejected" + ? "Rejected" + : "Awaiting Verification"; + + els.statusBadge.className = "status-badge"; + els.statusBadge.classList.add(status); +} + +function renderSocialLinks(user) { + const link = + user.website || + user.socialLink || + user.socialLinks?.[0] || + ""; + + if (!link) { + els.socialLinks.textContent = "—"; + return; + } + + els.socialLinks.innerHTML = ` + + ${link} + + `; +} + +function setFeedback(message, type = "") { + els.feedback.textContent = message; + els.feedback.className = "feedback"; + if (type) { + els.feedback.classList.add(type); + } +} + +function populateOrganizer(user) { + const status = getVerificationStatus(user); + + currentOrganizer = user; + + els.orgTitle.textContent = getDisplayName(user); + els.orgName.textContent = getDisplayName(user); + els.email.textContent = user.email || "—"; + els.phoneNumber.textContent = + user.phoneNumber || user.phone || "—"; + els.location.textContent = getLocation(user); + els.description.textContent = + user.description || + user.bio || + "No organization description available."; + + renderSocialLinks(user); + setStatusBadge(status); + + if (status === "verified") { + els.approveBtn.disabled = true; + } + + if (status === "rejected") { + els.rejectBtn.disabled = true; + } +} + +async function loadOrganizerDetails() { + if (!organizerId) { + setFeedback("No organizer ID provided.", "error"); + els.rejectBtn.disabled = true; + els.approveBtn.disabled = true; + return; + } + + try { + let payload; + + try { + payload = await apiRequest("/user"); + } catch { + payload = await apiRequest("/users"); + } + + const users = normalizeUsers(payload); + + const organizer = users.find( + (user) => String(user._id || user.id) === String(organizerId) + ); + + if (!organizer) { + throw new Error("Organizer not found"); + } + + populateOrganizer(organizer); + } catch (error) { + els.orgTitle.textContent = "Unable to load organizer"; + els.description.textContent = "Failed to fetch organizer details."; + setFeedback(error.message || "Failed to load organizer details.", "error"); + els.rejectBtn.disabled = true; + els.approveBtn.disabled = true; + } +} + +async function approveOrganizer() { + if (!organizerId) return; + + try { + els.approveBtn.disabled = true; + els.rejectBtn.disabled = true; + + await apiRequest(`/admin/organizers/${organizerId}/approve`, { + method: "PATCH" + }); + + setStatusBadge("verified"); + setFeedback("Organizer approved successfully.", "success"); + + setTimeout(() => { + window.location.href = "verification-queue.html"; + }, 900); + } catch (error) { + els.approveBtn.disabled = false; + els.rejectBtn.disabled = false; + setFeedback(error.message || "Failed to approve organizer.", "error"); + } +} + +async function rejectOrganizer() { + if (!organizerId) return; + + try { + els.approveBtn.disabled = true; + els.rejectBtn.disabled = true; + + await apiRequest(`/admin/organizers/${organizerId}/reject`, { + method: "PATCH" + }); + + setStatusBadge("rejected"); + setFeedback("Organizer rejected successfully.", "success"); + + setTimeout(() => { + window.location.href = "verification-queue.html"; + }, 900); + } catch (error) { + els.approveBtn.disabled = false; + els.rejectBtn.disabled = false; + setFeedback(error.message || "Failed to reject organizer.", "error"); + } +} + +els.approveBtn.addEventListener("click", approveOrganizer); +els.rejectBtn.addEventListener("click", rejectOrganizer); + +document.addEventListener("DOMContentLoaded", loadOrganizerDetails); diff --git a/Admin/verification/verification-queue.css b/Admin/verification/verification-queue.css new file mode 100644 index 0000000..6b24e05 --- /dev/null +++ b/Admin/verification/verification-queue.css @@ -0,0 +1,433 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: "Poppins", sans-serif; +} + +body { + background: #edf2f6; + color: #172033; +} + +.dashboard { + display: flex; + min-height: 100vh; +} + +.sidebar { + width: 15rem; + background: #223f6b; + color: #ffffff; + padding: 1.8rem 0.9rem; +} + +.sidebar-logo { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.35rem; + margin-bottom: 2.4rem; +} + +.sidebar-logo img { + width: 3.1rem; + height: 3.1rem; + object-fit: contain; +} + +.sidebar-menu { + list-style: none; +} + +.sidebar-menu li { + margin-bottom: 0.8rem; +} + +.sidebar-menu li a, +.logout-btn { + display: flex; + align-items: center; + gap: 0.85rem; + text-decoration: none; + color: #ffffff; + padding: 0.9rem 0.85rem; + border-radius: 0.45rem; + font-size: 0.95rem; + transition: background 0.2s ease; + width: 100%; + border: none; + background: transparent; + cursor: pointer; +} + +.sidebar-menu li a i, +.logout-btn i { + width: 1rem; + text-align: center; +} + +.sidebar-menu li.active a, +.sidebar-menu li a:hover { + background: #1d7de2; +} + +.logout-btn:hover { + background: #ef2f2f; +} + +.content { + flex: 1; + display: flex; + flex-direction: column; +} + +.topbar { + height: 5.8rem; + background: #ffffff; + border-bottom: 0.0625rem solid #e6e8eb; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 1.8rem; +} + +.search { + width: 21rem; + height: 2.9rem; + display: flex; + align-items: center; + gap: 0.75rem; + border: 0.0625rem solid #d9dde3; + border-radius: 0.35rem; + padding: 0 0.9rem; + background: #ffffff; +} + +.search i { + color: #6b7280; + font-size: 0.95rem; +} + +.search input { + width: 100%; + border: none; + outline: none; + background: transparent; + font-size: 0.95rem; + color: #374151; +} + +.top-icons { + display: flex; + align-items: center; + gap: 1rem; + color: #111827; +} + +.admin-mini-profile { + display: flex; + align-items: center; + gap: 0.6rem; +} + +.admin-mini-profile img { + width: 2.3rem; + height: 2.3rem; + border-radius: 50%; + object-fit: cover; +} + +.admin-mini-profile span { + font-size: 0.85rem; + font-weight: 600; +} + +.page-header { + padding: 2rem 1.4rem 1rem; + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; + flex-wrap: wrap; +} + +.page-header h1 { + font-size: 2rem; + font-weight: 700; + margin-bottom: 0.3rem; +} + +.page-header h3 { + font-size: 1rem; + margin-bottom: 0.3rem; +} + +.page-header p { + font-size: 0.82rem; + color: #6b7280; +} + +.filter-buttons { + display: flex; + gap: 0.8rem; + flex-wrap: wrap; +} + +.filter-btn { + border: 0.0625rem solid #d9dde3; + background: #ffffff; + color: #374151; + padding: 0.75rem 1rem; + border-radius: 0.4rem; + font-size: 0.8rem; + cursor: pointer; +} + +.filter-btn.active { + background: #1d7de2; + color: #ffffff; + border-color: #1d7de2; +} + +.table-card { + margin: 0 1.4rem 2rem; + background: #ffffff; + border-radius: 0.6rem; + overflow: hidden; +} + +table { + width: 100%; + border-collapse: collapse; +} + +thead { + background: #f4f6f8; +} + +th, +td { + text-align: left; + padding: 1rem 1.4rem; +} + +th { + font-size: 0.62rem; + letter-spacing: 0.08em; + color: #7b8492; + font-weight: 700; +} + +td { + font-size: 0.72rem; + color: #172033; + border-top: 0.0625rem solid #eef1f3; +} + +.badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 7rem; + padding: 0.35rem 0.7rem; + border-radius: 0.45rem; + color: #ffffff; + font-size: 0.62rem; + font-weight: 700; +} + +.badge.awaiting { + background: #f59e0b; +} + +.badge.verified { + background: #16a34a; +} + +.badge.approved { + background: #16a34a; +} + +.badge.rejected { + background: #dc2626; +} + +.view { + border: none; + background: #1d7de2; + color: #ffffff; + padding: 0.55rem 0.9rem; + border-radius: 0.35rem; + font-size: 0.72rem; + cursor: pointer; +} + +.view:hover { + opacity: 0.94; +} + +.empty-state { + text-align: center; + padding: 3rem 1rem; +} + +.empty-icon { + width: 4rem; + height: 4rem; + margin: 0 auto 1rem; + border-radius: 50%; + background: #e8f0fb; + color: #1d7de2; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.4rem; +} + +.empty-state h2 { + font-size: 1.1rem; + margin-bottom: 0.4rem; +} + +.empty-state p { + color: #6b7280; + font-size: 0.85rem; +} + +.hidden { + display: none !important; +} + +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(15, 23, 42, 0.45); + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; + z-index: 999; +} + +.modal-card { + width: 100%; + max-width: 25rem; + background: #ffffff; + border-radius: 1rem; + padding: 1.5rem; + position: relative; + text-align: center; + box-shadow: 0 1rem 2.5rem rgba(0, 0, 0, 0.15); +} + +.modal-close-btn { + position: absolute; + top: 0.7rem; + right: 0.9rem; + border: none; + background: transparent; + font-size: 1.5rem; + color: #6b7280; + cursor: pointer; +} + +.modal-icon-wrap { + width: 3.5rem; + height: 3.5rem; + margin: 0 auto 1rem; + border-radius: 50%; + background: #fee2e2; + color: #dc2626; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.25rem; +} + +.modal-card h3 { + font-size: 1.2rem; + margin-bottom: 0.5rem; + color: #172033; +} + +.modal-card p { + font-size: 0.92rem; + color: #6b7280; + margin-bottom: 1.3rem; +} + +.modal-actions { + display: flex; + gap: 0.8rem; + justify-content: center; +} + +.modal-btn { + border: none; + border-radius: 0.55rem; + padding: 0.8rem 1rem; + font-size: 0.9rem; + cursor: pointer; + min-width: 8rem; +} + +.modal-btn.secondary { + background: #e5e7eb; + color: #172033; +} + +.modal-btn.danger { + background: #dc2626; + color: #ffffff; +} + +@media (max-width: 75rem) { + .table-card { + overflow-x: auto; + } + + table { + min-width: 46rem; + } +} + +@media (max-width: 62rem) { + .sidebar { + display: none; + } + + .topbar { + padding: 0 1rem; + } + + .search { + width: 16rem; + } + + .page-header { + padding: 2rem 1rem 1rem; + } + + .table-card { + margin: 0 1rem 2rem; + } + + .admin-mini-profile span { + display: none; + } +} + +@media (max-width: 36rem) { + .search { + width: 12rem; + } + + .modal-actions { + flex-direction: column; + } + + .modal-btn { + width: 100%; + } +} diff --git a/Admin/verification/verification-queue.html b/Admin/verification/verification-queue.html new file mode 100644 index 0000000..6da050d --- /dev/null +++ b/Admin/verification/verification-queue.html @@ -0,0 +1,137 @@ + + + + + + AIDLoop Verification + + + + + + + + +
+ + +
+
+ + +
+ +
+ Admin Avatar + Loading... +
+
+
+ + + +
+ + + + + + + + + + + + + + + + +
Organization NameContact EmailLocationStatusActions
Loading verification queue...
+ + +
+
+
+ + + + + + \ No newline at end of file diff --git a/Admin/verification/verification-queue.js b/Admin/verification/verification-queue.js new file mode 100644 index 0000000..cdb9a62 --- /dev/null +++ b/Admin/verification/verification-queue.js @@ -0,0 +1,601 @@ +// const API_BASE_URL = "https://aidloop-backend.onrender.com/api"; + +// const els = { +// searchInput: document.getElementById("searchInput"), +// orgTable: document.getElementById("orgTable"), +// orgTableWrap: document.getElementById("orgTableWrap"), +// pendingCount: document.getElementById("pendingCount"), +// adminName: document.getElementById("adminName"), +// adminAvatar: document.getElementById("adminAvatar"), +// filterButtons: document.querySelectorAll(".filter-btn"), +// emptyState: document.getElementById("emptyState"), +// logoutBtn: document.getElementById("logoutBtn"), +// logoutModal: document.getElementById("logoutModal"), +// closeLogoutModal: document.getElementById("closeLogoutModal"), +// cancelLogout: document.getElementById("cancelLogout"), +// confirmLogout: document.getElementById("confirmLogout") +// }; + +// let organizers = []; +// let currentFilter = "awaiting"; + +// async function apiRequest(endpoint, options = {}) { +// const response = await fetch(`${API_BASE_URL}${endpoint}`, { +// credentials: "include", +// headers: { +// "Content-Type": "application/json", +// ...(options.headers || {}) +// }, +// ...options +// }); + +// const contentType = response.headers.get("content-type") || ""; +// const data = contentType.includes("application/json") +// ? await response.json() +// : await response.text(); + +// if (!response.ok) { +// throw new Error( +// (data && data.message) || +// (data && data.error) || +// "Request failed" +// ); +// } + +// return data; +// } + +// function normalizeUsers(payload) { +// if (Array.isArray(payload)) return payload; +// if (Array.isArray(payload?.users)) return payload.users; +// if (Array.isArray(payload?.data)) return payload.data; +// return []; +// } + +// function getVerificationStatus(user) { +// const status = String(user.status || "").toLowerCase(); +// const approvalStatus = String(user.approvalStatus || "").toLowerCase(); +// const isVerified = Boolean(user.isVerified); + +// if (status === "rejected" || approvalStatus === "rejected") { +// return "rejected"; +// } + +// if ( +// status === "verified" || +// status === "approved" || +// approvalStatus === "verified" || +// approvalStatus === "approved" || +// isVerified +// ) { +// return "verified"; +// } + +// return "awaiting"; +// } + +// function getLocation(user) { +// if (typeof user.location === "string" && user.location.trim()) { +// return user.location; +// } + +// if (user.location && typeof user.location === "object") { +// return ( +// user.location.city || +// user.location.state || +// user.location.venue || +// "—" +// ); +// } + +// return user.city || user.state || "—"; +// } + +// function getDisplayName(user) { +// return user.fullName || user.name || user.organizationName || "Unnamed Organizer"; +// } + +// function badgeText(status) { +// if (status === "verified") return "Verified"; +// if (status === "rejected") return "Rejected"; +// return "Awaiting Verification"; +// } + +// function updatePendingCount() { +// const count = organizers.filter( +// (organizer) => organizer._verificationStatus === "awaiting" +// ).length; + +// els.pendingCount.textContent = count; +// } + +// function renderTable() { +// const query = els.searchInput.value.trim().toLowerCase(); + +// const filtered = organizers.filter((organizer) => { +// const matchesFilter = +// currentFilter === "all" +// ? true +// : organizer._verificationStatus === currentFilter; + +// const searchableText = ` +// ${getDisplayName(organizer)} +// ${organizer.email || ""} +// ${getLocation(organizer)} +// ${organizer._verificationStatus} +// `.toLowerCase(); + +// return matchesFilter && searchableText.includes(query); +// }); + +// if (!filtered.length) { +// els.orgTableWrap.style.display = "none"; +// els.emptyState.style.display = "block"; +// return; +// } + +// els.orgTableWrap.style.display = "table"; +// els.emptyState.style.display = "none"; + +// els.orgTable.innerHTML = filtered +// .map((organizer) => { +// const id = organizer._id || organizer.id || ""; +// const status = organizer._verificationStatus; + +// return ` +// +// ${getDisplayName(organizer)} +// ${organizer.email || "—"} +// ${getLocation(organizer)} +// ${badgeText(status)} +// +// +// +// +// `; +// }) +// .join(""); + +// attachViewDetailsHandlers(); +// } + +// function attachViewDetailsHandlers() { +// document.querySelectorAll(".view").forEach((button) => { +// button.addEventListener("click", () => { +// const organizerId = button.dataset.id; +// window.location.href = `verification-details.html?id=${encodeURIComponent(organizerId)}`; +// }); +// }); +// } + +// async function loadAdminProfile() { +// try { +// let profile; + +// try { +// profile = await apiRequest("/users/me"); +// } catch { +// profile = await apiRequest("/user/me"); +// } + +// els.adminName.textContent = +// profile.fullName || profile.name || "Admin"; + +// if (profile.profileImage) { +// els.adminAvatar.src = profile.profileImage; +// } +// } catch (error) { +// console.error("Failed to load admin profile:", error.message); +// } +// } + +// async function loadVerificationQueue() { +// try { +// let usersPayload; + +// try { +// usersPayload = await apiRequest("/user"); +// } catch { +// usersPayload = await apiRequest("/users"); +// } + +// const users = normalizeUsers(usersPayload); + +// organizers = users +// .filter((user) => String(user.role || "").toLowerCase() === "organizer") +// .map((user) => ({ +// ...user, +// _verificationStatus: getVerificationStatus(user) +// })); + +// updatePendingCount(); +// renderTable(); +// } catch (error) { +// console.error("Failed to load verification queue:", error.message); +// els.orgTable.innerHTML = ` +// +// Failed to load verification queue. +// +// `; +// } +// } + +// function bindFilters() { +// els.filterButtons.forEach((button) => { +// button.addEventListener("click", () => { +// els.filterButtons.forEach((btn) => btn.classList.remove("active")); +// button.classList.add("active"); +// currentFilter = button.dataset.filter; +// renderTable(); +// }); +// }); +// } + +// function openLogoutModal() { +// els.logoutModal.classList.remove("hidden"); +// } + +// function closeLogoutModal() { +// els.logoutModal.classList.add("hidden"); +// els.confirmLogout.disabled = false; +// els.confirmLogout.textContent = "Yes, Log out"; +// } + +// async function handleLogout() { +// try { +// els.confirmLogout.disabled = true; +// els.confirmLogout.textContent = "Logging out..."; + +// await apiRequest("/auth/logout", { +// method: "POST" +// }); +// } catch (error) { +// console.warn("Logout failed:", error.message); +// } finally { +// localStorage.clear(); +// sessionStorage.clear(); +// window.location.href = "../../index.html"; +// } +// } + +// function bindUI() { +// els.searchInput.addEventListener("input", renderTable); +// bindFilters(); + +// els.logoutBtn.addEventListener("click", openLogoutModal); +// els.closeLogoutModal.addEventListener("click", closeLogoutModal); +// els.cancelLogout.addEventListener("click", closeLogoutModal); +// els.confirmLogout.addEventListener("click", handleLogout); + +// els.logoutModal.addEventListener("click", (event) => { +// if (event.target === els.logoutModal) { +// closeLogoutModal(); +// } +// }); + +// document.addEventListener("keydown", (event) => { +// if (event.key === "Escape" && !els.logoutModal.classList.contains("hidden")) { +// closeLogoutModal(); +// } +// }); +// } + +// document.addEventListener("DOMContentLoaded", async () => { +// bindUI(); +// await loadAdminProfile(); +// await loadVerificationQueue(); +// }); + + + + + + + + + + + +const API_BASE_URL = "https://aidloop-backend.onrender.com/api"; + +const els = { + searchInput: document.getElementById("searchInput"), + orgTable: document.getElementById("orgTable"), + orgTableWrap: document.getElementById("orgTableWrap"), + pendingCount: document.getElementById("pendingCount"), + adminName: document.getElementById("adminName"), + adminAvatar: document.getElementById("adminAvatar"), + filterButtons: document.querySelectorAll(".filter-btn"), + emptyState: document.getElementById("emptyState"), + logoutBtn: document.getElementById("logoutBtn"), + logoutModal: document.getElementById("logoutModal"), + closeLogoutModal: document.getElementById("closeLogoutModal"), + cancelLogout: document.getElementById("cancelLogout"), + confirmLogout: document.getElementById("confirmLogout") +}; + +let organizers = []; +let currentFilter = "awaiting"; + +async function apiRequest(endpoint, options = {}) { + const response = await fetch(`${API_BASE_URL}${endpoint}`, { + credentials: "include", + headers: { + "Content-Type": "application/json", + ...(options.headers || {}) + }, + ...options + }); + + const contentType = response.headers.get("content-type") || ""; + const data = contentType.includes("application/json") + ? await response.json() + : await response.text(); + + if (!response.ok) { + throw new Error( + (data && data.message) || + (data && data.error) || + "Request failed" + ); + } + + return data; +} + +function normalizeUsers(payload) { + if (Array.isArray(payload)) return payload; + if (Array.isArray(payload?.users)) return payload.users; + if (Array.isArray(payload?.data)) return payload.data; + return []; +} + +function getVerificationStatus(user) { + const status = String(user.status || "").toLowerCase(); + const approvalStatus = String(user.approvalStatus || "").toLowerCase(); + const isVerified = Boolean(user.isVerified); + + if (status === "rejected" || approvalStatus === "rejected") { + return "rejected"; + } + + if ( + status === "approved" || + status === "verified" || + approvalStatus === "approved" || + approvalStatus === "verified" || + isVerified + ) { + return "approved"; + } + + return "awaiting"; +} + +function getLocation(user) { + if (typeof user.location === "string" && user.location.trim()) { + return user.location; + } + + if (user.location && typeof user.location === "object") { + return ( + user.location.city || + user.location.state || + user.location.venue || + "—" + ); + } + + return user.city || user.state || "—"; +} + +function getDisplayName(user) { + return user.fullName || user.name || user.organizationName || "Unnamed Organizer"; +} + +function badgeText(status) { + if (status === "approved") return "Approved"; + if (status === "rejected") return "Rejected"; + return "Awaiting Verification"; +} + +function getSortDate(user) { + return new Date( + user.createdAt || + user.updatedAt || + user.dateCreated || + 0 + ).getTime(); +} + +function updatePendingCount() { + const count = organizers.filter( + (organizer) => organizer._verificationStatus === "awaiting" + ).length; + + els.pendingCount.textContent = count; +} + +function renderTable() { + const query = els.searchInput.value.trim().toLowerCase(); + + const filtered = organizers.filter((organizer) => { + const matchesFilter = + currentFilter === "all" + ? true + : organizer._verificationStatus === currentFilter; + + const searchableText = ` + ${getDisplayName(organizer)} + ${organizer.email || ""} + ${getLocation(organizer)} + ${organizer._verificationStatus} + `.toLowerCase(); + + return matchesFilter && searchableText.includes(query); + }); + + if (!filtered.length) { + els.orgTableWrap.style.display = "none"; + els.emptyState.style.display = "block"; + return; + } + + els.orgTableWrap.style.display = "table"; + els.emptyState.style.display = "none"; + + els.orgTable.innerHTML = filtered + .map((organizer) => { + const id = organizer._id || organizer.id || ""; + const status = organizer._verificationStatus; + + return ` + + ${getDisplayName(organizer)} + ${organizer.email || "—"} + ${getLocation(organizer)} + ${badgeText(status)} + + + + + `; + }) + .join(""); + + attachViewDetailsHandlers(); +} + +function attachViewDetailsHandlers() { + document.querySelectorAll(".view").forEach((button) => { + button.addEventListener("click", () => { + const organizerId = button.dataset.id; + window.location.href = `verification-details.html?id=${encodeURIComponent(organizerId)}`; + }); + }); +} + +async function loadAdminProfile() { + try { + let profile; + + try { + profile = await apiRequest("/users/me"); + } catch { + profile = await apiRequest("/user/me"); + } + + els.adminName.textContent = + profile.fullName || profile.name || "Admin"; + + if (profile.profileImage) { + els.adminAvatar.src = profile.profileImage; + } + } catch (error) { + console.error("Failed to load admin profile:", error.message); + } +} + +async function loadVerificationQueue() { + try { + let usersPayload; + + try { + usersPayload = await apiRequest("/user"); + } catch { + usersPayload = await apiRequest("/users"); + } + + const users = normalizeUsers(usersPayload); + + organizers = users + .filter((user) => String(user.role || "").toLowerCase() === "organizer") + .map((user) => ({ + ...user, + _verificationStatus: getVerificationStatus(user) + })) + .sort((a, b) => getSortDate(b) - getSortDate(a)); // newest first + + updatePendingCount(); + renderTable(); + } catch (error) { + console.error("Failed to load verification queue:", error.message); + els.orgTable.innerHTML = ` + + Failed to load verification queue. + + `; + } +} + +function bindFilters() { + els.filterButtons.forEach((button) => { + button.addEventListener("click", () => { + els.filterButtons.forEach((btn) => btn.classList.remove("active")); + button.classList.add("active"); + currentFilter = button.dataset.filter; + renderTable(); + }); + }); +} + +function openLogoutModal() { + if (!els.logoutModal) return; + els.logoutModal.classList.remove("hidden"); +} + +function closeLogoutModal() { + if (!els.logoutModal) return; + els.logoutModal.classList.add("hidden"); + if (els.confirmLogout) { + els.confirmLogout.disabled = false; + els.confirmLogout.textContent = "Yes, Log out"; + } +} + +async function handleLogout() { + try { + if (els.confirmLogout) { + els.confirmLogout.disabled = true; + els.confirmLogout.textContent = "Logging out..."; + } + + await apiRequest("/auth/logout", { + method: "POST" + }); + } catch (error) { + console.warn("Logout failed:", error.message); + } finally { + localStorage.clear(); + sessionStorage.clear(); + window.location.href = "../../index.html"; + } +} + +function bindUI() { + els.searchInput?.addEventListener("input", renderTable); + bindFilters(); + + els.logoutBtn?.addEventListener("click", openLogoutModal); + els.closeLogoutModal?.addEventListener("click", closeLogoutModal); + els.cancelLogout?.addEventListener("click", closeLogoutModal); + els.confirmLogout?.addEventListener("click", handleLogout); + + els.logoutModal?.addEventListener("click", (event) => { + if (event.target === els.logoutModal) { + closeLogoutModal(); + } + }); + + document.addEventListener("keydown", (event) => { + if (event.key === "Escape" && els.logoutModal && !els.logoutModal.classList.contains("hidden")) { + closeLogoutModal(); + } + }); +} + +document.addEventListener("DOMContentLoaded", async () => { + bindUI(); + await loadAdminProfile(); + await loadVerificationQueue(); +}); \ No newline at end of file diff --git a/Contact.html b/Contact.html deleted file mode 100644 index e69de29..0000000 diff --git a/HowItWorks.html b/HowItWorks.html deleted file mode 100644 index 4bd5661..0000000 --- a/HowItWorks.html +++ /dev/null @@ -1,247 +0,0 @@ - - - - - - AIDLOOP-FRONTENDWEB - - - - - - - - -
- -
- -
- -
- -

Recruit Verified

- -

- Volun-teers -

- -

for Your Events

- -

- Create and manage volunteers, connect with verified volunteers, - track attendance, and reward effort with certificates — all in one - platform designed to maximize your community impact. -

- -
- - -
- -
- - -
- Volunteer platform preview -
- -
- - - - - -
-
-

How it Works

-
- -
-
- - - -

Sign Up

-

Sign up your organization to start hosting - volunteer events. Submit your details - and get verified by the platform. -

-
- - - -
- - - -

Create Volunteer Event

-

- Create volunteer apportunities using a simple - event template. Add the event details, roles, and - number of volunteers needed. -

-
- -
- - - -

Manage Volunteers

-

- Track registrations, mark attendance, rate - volunteers, and issue certificates from your - organizer dashboard. -

-
-
-
- - -
-
-

Benefits

-
- -
-
- - - -
Verified Volunteers
-

- Connect with volunteers who are registered and - tracked through the platform -

-
-
- - -
- - - -
Easy Event Creation
-

- Create events quickly using a structured template - designed for volunteer activities. -

-
-
- - -
- - - -
Attendance Tracking
-

- Monitor volunteer participation and keep records for each event. -

-
-
- - -
- - - -
Volunteer Certificates
-

- Recognize volunteers by issuing certificates after successful - event participation. -

- -
-
-
- - -
-
-
-

Start Organizing Volunteer Events Today

-
- -

- Join organization already using AidLoop to manage volunteer events - and create meaningful community impact. -

- - -
- -
- - - - - - - - \ No newline at end of file diff --git a/Images/WhatsApp Image 2026-03-08 at 15.32.16.jpeg b/Images/WhatsApp Image 2026-03-08 at 15.32.16.jpeg deleted file mode 100644 index f251515..0000000 Binary files a/Images/WhatsApp Image 2026-03-08 at 15.32.16.jpeg and /dev/null differ diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 02b72bb..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 aidloop - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/Login.html b/Login.html deleted file mode 100644 index e69de29..0000000 diff --git a/Organizer/certificates/organizer-certificates.css b/Organizer/certificates/organizer-certificates.css new file mode 100644 index 0000000..1a4be4e --- /dev/null +++ b/Organizer/certificates/organizer-certificates.css @@ -0,0 +1,317 @@ +*{ + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: "Poppins", sans-serif; +} + +body { + background: #f4f7fb; + color: #1f2f46; +} + +.dashboard-layout { + display: flex; + min-height: 100vh; +} + +.sidebar { + width: 15rem; + background: #1f3b63; + color: #ffffff; + padding: 1.5rem 1rem; + flex-shrink: 0; +} + +.sidebar-logo { + text-align: center; + margin-bottom: 2rem; +} + +.sidebar-logo img { + width: 5.5rem; + max-width: 100%; +} + +.sidebar-menu { + list-style: none; + display: flex; + flex-direction: column; + gap: 0.55rem; +} + +.sidebar-menu li a, +.sidebar-logout { + width: 100%; + display: flex; + align-items: center; + gap: 0.85rem; + color: #ffffff; + text-decoration: none; + padding: 0.95rem 1rem; + border-radius: 0.65rem; + font-size: 0.96rem; + border: none; + background: transparent; + cursor: pointer; +} + +.sidebar-menu li.active a, +.sidebar-menu li a:hover { + background: #1f80ea; +} + +.sidebar-logout:hover { + background: #dc2626; +} + +.main-content { + flex: 1; + display: flex; + flex-direction: column; +} + +.topbar { + background: #ffffff; + padding: 2rem 2.2rem 1.2rem; + display: flex; + justify-content: space-between; + align-items: flex-start; +} + +.page-title { + font-size: 2rem; + font-weight: 700; + color: #223f6b; +} + +.page-subtitle { + margin-top: 0.3rem; + font-size: 0.9rem; + color: #6b7280; +} + +.topbar-right { + display: flex; + align-items: center; + gap: 0.9rem; +} + +.icon-btn { + width: 2.7rem; + height: 2.7rem; + border: none; + border-radius: 50%; + background: transparent; + color: #223f6b; + font-size: 1.35rem; + cursor: pointer; +} + +.page-content { + padding: 2.2rem; +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 1.4rem; + max-width: 40rem; + margin-bottom: 2rem; +} + +.stat-card { + background: #ffffff; + border-radius: 0.9rem; + padding: 1.2rem; + display: flex; + align-items: center; + gap: 1rem; + box-shadow: 0 0.15rem 0.45rem rgba(0, 0, 0, 0.05); +} + +.stat-icon { + width: 3rem; + height: 3rem; + border-radius: 0.7rem; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.15rem; + flex-shrink: 0; +} + +.stat-icon.gold { + background: #fff0d8; + color: #d29a1f; +} + +.stat-icon.green { + background: #dff2e4; + color: #2e8b57; +} + +.stat-icon.pink { + background: #f8d9dd; + color: #a23c4b; +} + +.stat-text p { + font-size: 0.72rem; + color: #8a94a6; + font-weight: 600; +} + +.stat-text h3 { + font-size: 1.9rem; + color: #223f6b; + font-weight: 700; + margin-top: 0.1rem; +} + +.table-wrapper { + background: #ffffff; + border-radius: 0.9rem; + overflow: hidden; + border: 0.08rem solid #d8dfe8; +} + +table { + width: 100%; + border-collapse: collapse; +} + +thead { + background: #eef2f6; +} + +th, +td { + padding: 1rem; + text-align: left; + font-size: 0.92rem; +} + +th { + color: #35527a; + font-weight: 600; +} + +tbody tr { + border-bottom: 0.08rem solid #d7dde6; +} + +tbody tr:last-child { + border-bottom: none; +} + +.person-cell { + display: flex; + align-items: center; + gap: 0.8rem; +} + +.avatar { + width: 2.2rem; + height: 2.2rem; + border-radius: 50%; + object-fit: cover; + flex-shrink: 0; +} + +.status-badge { + display: inline-block; + min-width: 6.8rem; + text-align: center; + padding: 0.58rem 0.95rem; + border-radius: 0.42rem; + color: #ffffff; + font-size: 0.84rem; + font-weight: 600; +} + +.status-issued { + background: #2e8b57; +} + +.status-pending { + background: #f5ae42; +} + +.row-actions { + text-align: center; +} + +.row-actions button, +.row-actions a { + border: none; + background: transparent; + color: #223f6b; + font-size: 1.1rem; + cursor: pointer; + text-decoration: none; +} + +.table-footer { + margin-top: 1rem; + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; + flex-wrap: wrap; + color: #4b5563; + font-size: 0.9rem; +} + +.pagination { + display: flex; + align-items: center; + gap: 0.35rem; + flex-wrap: wrap; +} + +.page-btn { + border: 0.08rem solid #d6dde6; + background: #ffffff; + color: #1f2f46; + padding: 0.42rem 0.75rem; + border-radius: 0.3rem; + cursor: pointer; + font-size: 0.8rem; +} + +.page-btn.active { + background: #1f80ea; + color: #ffffff; + border-color: #1f80ea; +} + +@media (max-width: 68rem) { + .stats-grid { + grid-template-columns: 1fr; + max-width: 100%; + } +} + +@media (max-width: 64rem) { + .dashboard-layout { + flex-direction: column; + } + + .sidebar { + width: 100%; + } + + .topbar { + flex-direction: column; + gap: 1rem; + } + + .table-wrapper { + overflow-x: auto; + } + + table { + min-width: 46rem; + } +} \ No newline at end of file diff --git a/Organizer/certificates/organizer-certificates.html b/Organizer/certificates/organizer-certificates.html new file mode 100644 index 0000000..e182e62 --- /dev/null +++ b/Organizer/certificates/organizer-certificates.html @@ -0,0 +1,169 @@ + + + + + + AIDLoop Organizer Certificates + + + + + + + + +
+ + +
+
+
+

Certificates

+

Manage volunteer certificates

+
+ +
+ + + +
+
+ +
+
+
+
+ +
+
+

TOTAL CERTIFICATES

+

0

+
+
+ +
+
+ +
+
+

ISSUED

+

0

+
+
+ +
+
+ +
+
+

PENDING

+

0

+
+
+
+ +
+ + + + + + + + + + + + + + + + +
NameEVENTDateStatus
Loading certificates...
+
+ + +
+
+
+ + + + + + + + + + + + + diff --git a/Organizer/certificates/organizer-certificates.js b/Organizer/certificates/organizer-certificates.js new file mode 100644 index 0000000..2ee07c8 --- /dev/null +++ b/Organizer/certificates/organizer-certificates.js @@ -0,0 +1,93 @@ +import { apiRequest, normalizeArray } from "../../assets/js/api.js"; +import { requireOrganizer } from "../../assets/js/auth.js"; +import { logout } from "../../assets/js/logout.js"; +import { ROUTES } from "../../assets/js/config.js"; +import { formatDate } from "../../assets/js/utils.js"; + +const els = { + totalCertificates: document.getElementById("totalCertificates"), + issuedCertificates: document.getElementById("issuedCertificates"), + pendingCertificates: document.getElementById("pendingCertificates"), + certificatesTable: document.getElementById("certificatesTable"), + tableCountText: document.getElementById("tableCountText"), + logoutBtn: document.getElementById("logoutBtn") +}; + +function rowKey(userId, eventId) { return `${String(userId)}::${String(eventId)}`; } +function avatar(user) { return user?.profileImage || "https://i.pravatar.cc/100?img=12"; } + +function normalizeIssued(records) { + return records.map((item) => ({ + status: "issued", + certificateId: item._id || item.id || item.certificateId || "", + userId: item.user?._id || item.user?.id || item.volunteer?._id || item.volunteer?.id || item.userId || "", + eventId: item.event?._id || item.event?.id || item.eventId || "", + userName: item.user?.fullName || item.user?.name || item.volunteer?.fullName || item.volunteer?.name || item.volunteerName || "Unknown Volunteer", + userAvatar: avatar(item.user || item.volunteer), + eventName: item.event?.name || item.eventName || "Untitled Event", + date: item.issuedAt || item.createdAt || item.event?.date || "" + })); +} + +function render(rows) { + els.totalCertificates.textContent = rows.length; + els.issuedCertificates.textContent = rows.filter((r) => r.status === "issued").length; + els.pendingCertificates.textContent = rows.filter((r) => r.status === "pending").length; + els.tableCountText.textContent = `Showing ${rows.length} of ${rows.length} certificates`; + els.certificatesTable.innerHTML = rows.map((row) => ` + +
${row.userName}${row.userName}
+ ${row.eventName} + ${formatDate(row.date, "long")} + ${row.status === "issued" ? "Issued" : "Pending"} + ${row.status === "issued" && row.certificateId ? `View` : "..."} + + `).join("") || `No certificate records found.`; +} + +document.addEventListener("DOMContentLoaded", async () => { + const organizer = await requireOrganizer(); + if (!organizer) return; + els.logoutBtn.addEventListener("click", () => logout(ROUTES.home)); + + try { + const eventsPayload = await apiRequest("/events"); + const allEvents = normalizeArray(eventsPayload, ["events"]); + const organizerId = String(organizer._id || organizer.id || ""); + const ownEvents = allEvents.filter((event) => { + if (typeof event.organizer === "object" && event.organizer) { + return String(event.organizer._id || event.organizer.id || "") === organizerId; + } + return String(event.organizerId || "") === organizerId; + }); + + const registrations = []; + for (const event of ownEvents) { + const data = await apiRequest(`/applications/events/${event._id || event.id}/registrations`); + const regs = Array.isArray(data) ? data : data.data || []; + regs.forEach((reg) => registrations.push({ ...reg, _eventId: event._id || event.id, _eventName: event.name || "Untitled Event", _eventDate: event.date })); + } + + let issuedPayload = []; + try { issuedPayload = await apiRequest("/certificates/my-certificates"); } catch { issuedPayload = []; } + const issuedRows = normalizeIssued(normalizeArray(issuedPayload, ["certificates"])); + const issuedKeys = new Set(issuedRows.map((r) => rowKey(r.userId, r.eventId))); + + const pendingRows = registrations + .filter((reg) => !issuedKeys.has(rowKey(reg.user?._id || reg.user?.id || "", reg._eventId || ""))) + .map((reg) => ({ + status: "pending", + certificateId: "", + userId: reg.user?._id || reg.user?.id || "", + eventId: reg._eventId, + userName: reg.user?.fullName || reg.user?.name || "Unknown Volunteer", + userAvatar: avatar(reg.user), + eventName: reg._eventName, + date: reg._eventDate || reg.createdAt || "" + })); + + render([...issuedRows, ...pendingRows].sort((a, b) => new Date(b.date || 0) - new Date(a.date || 0))); + } catch { + els.certificatesTable.innerHTML = `Failed to load certificates.`; + } +}); diff --git a/Organizer/dashboard/organizer-dashboard.css b/Organizer/dashboard/organizer-dashboard.css new file mode 100644 index 0000000..242868d --- /dev/null +++ b/Organizer/dashboard/organizer-dashboard.css @@ -0,0 +1,277 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: "Poppins", sans-serif; +} + +body { + background: #f4f7fb; + color: #1f2f46; +} + +.dashboard-layout { + display: flex; + min-height: 100vh; +} + +.sidebar { + width: 15.5rem; + background: #1f3b63; + color: #fff; + padding: 1.5rem 1rem; + flex-shrink: 0; +} + +.sidebar-logo { + text-align: center; + margin-bottom: 2rem; +} + +.sidebar-logo img { + width: 6rem; +} + +.sidebar-menu { + list-style: none; + display: flex; + flex-direction: column; + gap: 0.55rem; +} + +.sidebar-menu li a, +.sidebar-logout { + width: 100%; + display: flex; + align-items: center; + gap: 0.85rem; + color: #fff; + text-decoration: none; + padding: 0.95rem 1rem; + border-radius: 0.65rem; + font-size: 0.98rem; + border: none; + background: transparent; + cursor: pointer; +} + +.sidebar-menu li.active a, +.sidebar-menu li a:hover { + background: #1f80ea; +} + +.sidebar-logout:hover { + background: #dc2626; +} + +.main-content { + flex: 1; + padding: 0; +} + +.topbar { + background: #fff; + padding: 2rem 2rem 1.2rem; + display: flex; + justify-content: space-between; + align-items: flex-start; +} + +.welcome-title { + font-size: 2rem; + font-weight: 700; + color: #223f6b; +} + +.welcome-subtitle { + color: #b7bec8; + font-size: 0.9rem; + margin-top: 0.3rem; +} + +.topbar-actions { + display: flex; + align-items: center; + gap: 1rem; +} + +.create-event-btn { + background: #223f6b; + color: #fff; + text-decoration: none; + padding: 1rem 1.5rem; + border-radius: 0.8rem; + font-weight: 600; +} + +.icon-btn { + width: 2.8rem; + height: 2.8rem; + border: none; + border-radius: 50%; + background: transparent; + color: #223f6b; + font-size: 1.5rem; + cursor: pointer; +} + +.profile-mini img { + width: 2.6rem; + height: 2.6rem; + border-radius: 50%; + object-fit: cover; +} + +.dashboard-section, +.recent-events-section { + padding: 2rem; +} + +.dashboard-section h2, +.recent-events-section h2 { + font-size: 1.9rem; + color: #223f6b; + margin-bottom: 1.5rem; +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 1.5rem; +} + +.stat-card { + background: #fff; + border: 0.08rem solid #9ca9bb; + border-radius: 0.9rem; + overflow: hidden; +} + +.stat-number { + text-align: center; + font-size: 2.2rem; + font-weight: 700; + padding: 1rem 0; + color: #111; +} + +.stat-footer { + border-top: 0.08rem solid #9ca9bb; + display: flex; + align-items: center; + gap: 0.6rem; + padding: 0.95rem 1rem; + color: #526176; + font-size: 0.82rem; +} + +.stat-footer i { + color: #1f80ea; + font-size: 1.15rem; +} + +.section-divider { + height: 0.08rem; + background: #aeb8c6; + margin-bottom: 2rem; +} + +.table-wrapper { + background: #fff; + border-radius: 0.9rem; + overflow: hidden; +} + +table { + width: 100%; + border-collapse: collapse; +} + +thead { + background: #eef2f6; +} + +th, td { + padding: 1.2rem 1.1rem; + text-align: left; + font-size: 0.95rem; +} + +th { + color: #35527a; + font-weight: 600; +} + +tbody tr { + border-bottom: 0.08rem solid #d7dde6; +} + +tbody tr:last-child { + border-bottom: none; +} + +.status-badge { + display: inline-block; + min-width: 7rem; + text-align: center; + padding: 0.65rem 1rem; + border-radius: 0.45rem; + color: #fff; + font-size: 0.9rem; + font-weight: 600; +} + +.status-upcoming { + background: #f5ae42; +} + +.status-published { + background: #26a65b; +} + +.status-draft { + background: #6f809b; +} + +.status-completed { + background: #1f80ea; +} + +.row-actions { + color: #223f6b; + font-size: 1.3rem; + text-align: center; +} + +@media (max-width: 68rem) { + .stats-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 52rem) { + .dashboard-layout { + flex-direction: column; + } + + .sidebar { + width: 100%; + } + + .topbar { + flex-direction: column; + gap: 1rem; + } + + .stats-grid { + grid-template-columns: 1fr; + } + + .table-wrapper { + overflow-x: auto; + } + + table { + min-width: 52rem; + } +} diff --git a/Organizer/dashboard/organizer-dashboard.html b/Organizer/dashboard/organizer-dashboard.html new file mode 100644 index 0000000..65562d6 --- /dev/null +++ b/Organizer/dashboard/organizer-dashboard.html @@ -0,0 +1,121 @@ + + + + + + AIDLoop Organizer Dashboard + + + + + + + + + +
+ + +
+
+
+

Welcome Back

+

An overview of your events and volunteer activity

+
+ +
+ Create Event + +
+ Organizer Avatar +
+
+
+ +
+

Dashboard

+ +
+
+
0
+ +
+ +
+
0
+ +
+ +
+
0
+ +
+ +
+
0
+ +
+
+
+ +
+
+

Recent Events

+ +
+ + + + + + + + + + + + + + + + +
Events NameLocationDateVolunteersStatus
Loading events...
+
+
+
+
+ + + + + diff --git a/Organizer/dashboard/organizer-dashboard.js b/Organizer/dashboard/organizer-dashboard.js new file mode 100644 index 0000000..1375a5e --- /dev/null +++ b/Organizer/dashboard/organizer-dashboard.js @@ -0,0 +1,63 @@ +import { apiRequest, normalizeArray } from "../../assets/js/api.js"; +import { requireOrganizer } from "../../assets/js/auth.js"; +import { logout } from "../../assets/js/logout.js"; +import { ROUTES } from "../../assets/js/config.js"; +import { formatDate, getLocationText } from "../../assets/js/utils.js"; + +const els = { + totalEvents: document.getElementById("totalEvents"), + upcomingEvents: document.getElementById("upcomingEvents"), + completedEvents: document.getElementById("completedEvents"), + totalVolunteers: document.getElementById("totalVolunteers"), + eventsTable: document.getElementById("eventsTable"), + logoutBtn: document.getElementById("logoutBtn") +}; + +let organizer; + +function getStatus(event) { + const raw = String(event.status || "").toLowerCase(); + if (raw === "draft") return "draft"; + if (raw === "cancelled" || raw === "canceled") return "cancelled"; + const eventDate = event.date ? new Date(event.date) : null; + if (raw === "published" && eventDate && eventDate < new Date()) return "completed"; + if (raw === "published") return "published"; + return "published"; +} + +document.addEventListener("DOMContentLoaded", async () => { + organizer = await requireOrganizer(); + if (!organizer) return; + + els.logoutBtn.addEventListener("click", () => logout(ROUTES.home)); + + try { + const payload = await apiRequest("/events"); + const allEvents = normalizeArray(payload, ["events"]); + const organizerId = String(organizer._id || organizer.id || ""); + const events = allEvents.filter((event) => { + if (typeof event.organizer === "object" && event.organizer) { + return String(event.organizer._id || event.organizer.id || "") === organizerId; + } + return String(event.organizerId || "") === organizerId; + }); + + const totalVolunteers = events.reduce((sum, event) => sum + (event.filledSlots ?? event.registrationsCount ?? 0), 0); + els.totalEvents.textContent = events.length; + els.upcomingEvents.textContent = events.filter((e) => getStatus(e) === "published").length; + els.completedEvents.textContent = events.filter((e) => getStatus(e) === "completed").length; + els.totalVolunteers.textContent = totalVolunteers; + + els.eventsTable.innerHTML = events.slice(0, 5).map((event) => ` + + ${event.name || "Untitled Event"} + ${getLocationText(event)} + ${formatDate(event.date, "long")} + ${event.filledSlots ?? event.registrationsCount ?? 0}/${event.volunteerSlots ?? 0} + ${getStatus(event)} + + `).join("") || `No events found.`; + } catch { + els.eventsTable.innerHTML = `Failed to load dashboard data.`; + } +}); diff --git a/Organizer/email-verified/email-verified.css b/Organizer/email-verified/email-verified.css new file mode 100644 index 0000000..5abfded --- /dev/null +++ b/Organizer/email-verified/email-verified.css @@ -0,0 +1,160 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: "Poppins", sans-serif; +} + +body { + background: #f5f7fa; + min-height: 100vh; +} + +.verified-page { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 2rem; +} + +.verified-card { + width: 100%; + max-width: 40rem; + min-height: 46rem; + background: #ffffff; + padding: 4rem 2rem; + text-align: center; + box-shadow: 0 0.625rem 2.5rem rgba(0, 0, 0, 0.05); +} + +.icon-wrap { + display: flex; + justify-content: center; + margin-bottom: 1.5rem; +} + +.success-icon { + width: 7rem; + height: 7rem; + border-radius: 50%; + background: #79e36d; + border: 0.1875rem solid #1fc933; + display: flex; + align-items: center; + justify-content: center; + position: relative; + clip-path: polygon( + 50% 0%, 61% 8%, 75% 8%, 82% 20%, 95% 25%, 92% 39%, 100% 50%, + 92% 61%, 95% 75%, 82% 80%, 75% 92%, 61% 92%, 50% 100%, 39% 92%, + 25% 92%, 18% 80%, 5% 75%, 8% 61%, 0% 50%, 8% 39%, 5% 25%, 18% 20%, + 25% 8%, 39% 8% + ); +} + +.success-icon i { + font-size: 2.3rem; + color: #ffffff; +} + +.verified-card h1 { + font-size: 2rem; + color: #223f6b; + margin-bottom: 1.2rem; + font-weight: 700; +} + +.mini-text { + min-height: 1rem; + margin-bottom: 2rem; + font-size: 0.8rem; + color: #6b7280; +} + +.divider { + width: 100%; + height: 0.0625rem; + background: #cfd5dc; + margin: 2rem 0 1.5rem; +} + +.message { + max-width: 26rem; + margin: 0 auto 2rem; + font-size: 1.25rem; + line-height: 1.7; + color: #444444; +} + +.change-email-link { + display: inline-block; + margin-bottom: 1rem; + font-size: 1.15rem; + color: #0d6efd; + text-decoration: none; + font-weight: 500; +} + +.change-email-link:hover { + text-decoration: underline; +} + +.back-link { + display: inline-block; + margin: 0.5rem 0 3rem; + font-size: 1.2rem; + color: #8b8b8b; + text-decoration: none; +} + +.back-link:hover { + color: #223f6b; +} + +.primary-btn { + width: 100%; + max-width: 28rem; + border: none; + background: #223f6b; + color: #ffffff; + padding: 1rem 1.25rem; + border-radius: 0.75rem; + font-size: 1.1rem; + font-weight: 600; + cursor: pointer; +} + +.primary-btn:hover { + opacity: 0.95; +} + +.primary-btn:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +@media (max-width: 40rem) { + .verified-card { + min-height: auto; + padding: 3rem 1.25rem; + } + + .verified-card h1 { + font-size: 1.6rem; + } + + .message { + font-size: 1rem; + } + + .change-email-link, + .back-link { + font-size: 1rem; + } + + .success-icon { + width: 6rem; + height: 6rem; + } +} + diff --git a/Organizer/email-verified/email-verified.html b/Organizer/email-verified/email-verified.html new file mode 100644 index 0000000..ed0eaf4 --- /dev/null +++ b/Organizer/email-verified/email-verified.html @@ -0,0 +1,52 @@ + + + + + + Email Verified Successfully + + + + + + + + +
+
+
+
+ +
+
+ +

Email Verified Successfully

+ +
+ +
+ +

+ Your email has been verified. You can now access your dashboard. +

+ + + Change Email Address + + +
+ + + Back to Login + + + +
+
+ + + + + diff --git a/Organizer/email-verified/email-verified.js b/Organizer/email-verified/email-verified.js new file mode 100644 index 0000000..d41b16f --- /dev/null +++ b/Organizer/email-verified/email-verified.js @@ -0,0 +1,56 @@ +import { apiRequest } from "../../assets/js/api.js"; +import { ROUTES } from "../../assets/js/config.js"; + +const els = { + continueBtn: document.getElementById("continueBtn"), + statusText: document.getElementById("statusText") +}; + +function setLoading(isLoading) { + els.continueBtn.disabled = isLoading; + els.continueBtn.textContent = isLoading + ? "Checking session..." + : "Continue to Dashboard"; +} + +async function checkUserSession() { + try { + setLoading(true); + + let user; + try { + user = await apiRequest("/users/me"); + } catch { + user = await apiRequest("/user/me"); + } + + if (user && String(user.role || "").toLowerCase() === "organizer") { + els.statusText.textContent = "You are already signed in."; + return true; + } + + els.statusText.textContent = ""; + return false; + } catch { + els.statusText.textContent = ""; + return false; + } finally { + setLoading(false); + } +} + +els.continueBtn.addEventListener("click", async () => { + const hasSession = await checkUserSession(); + + if (hasSession) { + window.location.href = ROUTES.organizerDashboard; + return; + } + + window.location.href = ROUTES.organizerLogin; +}); + +document.addEventListener("DOMContentLoaded", async () => { + await checkUserSession(); +}); + diff --git a/Organizer/events/cancel-event.css b/Organizer/events/cancel-event.css new file mode 100644 index 0000000..27b4ba6 --- /dev/null +++ b/Organizer/events/cancel-event.css @@ -0,0 +1,133 @@ + + +.page-title { + font-size: 2rem; + font-weight: 700; + color: #223f6b; +} + + +.cancel-section { + padding: 2rem; + max-width: 700px; +} + +.warning { + text-align: center; + margin-bottom: 2rem; +} + +.warning i { + font-size: 3rem; + color: orange; +} + +.warning p { + margin-top: 1rem; + color: #333; +} + +.reasons h3 { + margin-bottom: 1rem; +} + +.checkbox-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 0.8rem; +} + +.reason-text { + margin-top: 2rem; +} + +textarea { + width: 100%; + height: 120px; + padding: 0.8rem; + border-radius: 8px; + border: 1px solid #ccc; +} + +.actions { + display: flex; + gap: 1rem; + margin-top: 2rem; +} + +.btn { + padding: 0.8rem 1.4rem; + border-radius: 6px; + border: none; + cursor: pointer; +} + +.btn.danger { + background: red; + color: #fff; +} + +.btn.secondary { + background: #2c4a74; + color: #fff; +} + +/* MODAL */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.4); + display: flex; + justify-content: center; + align-items: center; + z-index: 999; +} + +.hidden { + display: none; +} + +.modal-card { + background: #fff; + padding: 2rem; + border-radius: 16px; + width: 400px; + max-width: 90%; + text-align: center; + position: relative; +} + +.close-btn { + position: absolute; + top: 10px; + right: 14px; + font-size: 1.4rem; + border: none; + background: none; + cursor: pointer; +} + +.warning-icon { + font-size: 3rem; + color: orange; + margin-bottom: 1rem; +} + +.modal-content p { + margin-bottom: 2rem; + color: #333; +} + +.btn.full { + width: 100%; + margin-bottom: 1rem; +} + +.btn.outline { + background: transparent; + border: 2px solid #2c4a74; + color: #2c4a74; +} \ No newline at end of file diff --git a/Organizer/events/cancel-event.html b/Organizer/events/cancel-event.html new file mode 100644 index 0000000..ca235a9 --- /dev/null +++ b/Organizer/events/cancel-event.html @@ -0,0 +1,140 @@ + + + + + + Cancel Event + + + + + + + + + + + + +
+ + + + + + +
+
+

Cancel Event

+
+ +
+ +
+ +

+ Cancelling this event will notify all registered volunteers and cannot be undone +

+
+ + +
+

Reasons for cancellation

+ +
+ + + + + + +
+
+ + +
+ + +
+ + +
+ + +
+ +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/Organizer/events/cancel-event.js b/Organizer/events/cancel-event.js new file mode 100644 index 0000000..d72993c --- /dev/null +++ b/Organizer/events/cancel-event.js @@ -0,0 +1,87 @@ +import { apiRequest } from "../../assets/js/api.js"; +import { requireRole } from "../../assets/js/auth.js"; +import { logout } from "../../assets/js/logout.js"; +import { ROUTES } from "../../assets/js/config.js"; + +const eventId = new URLSearchParams(window.location.search).get("id"); + +const els = { + cancelBtn: document.getElementById("cancelEventBtn"), + goBackBtn: document.getElementById("goBackBtn"), + reasonText: document.getElementById("reasonText"), + logoutBtn: document.getElementById("logoutBtn"), + confirmModal: document.getElementById("confirmModal"), + confirmCancel: document.getElementById("confirmCancel"), + closeModal: document.getElementById("closeModal"), + cancelModal: document.getElementById("cancelModal") +}; + +function getSelectedReasons() { + return [...document.querySelectorAll("input[type='checkbox']:checked")] + .map((cb) => cb.value); +} + +function openModal() { + els.confirmModal.classList.remove("hidden"); +} + +function hideModal() { + els.confirmModal.classList.add("hidden"); +} + +async function cancelEvent() { + const reasons = getSelectedReasons(); + const text = els.reasonText.value.trim(); + + if (!reasons.length && !text) { + alert("Please provide a reason"); + hideModal(); + return; + } + + const reason = [...reasons, text].filter(Boolean).join(", "); + + try { + els.confirmCancel.disabled = true; + els.confirmCancel.textContent = "Cancelling..."; + + await apiRequest(`/events/${eventId}/cancel`, { + method: "PATCH", + body: JSON.stringify({ reason }) + }); + + alert("Event cancelled successfully"); + window.location.href = ROUTES.eventListing; + } catch (err) { + alert(err.message || "Failed to cancel event"); + } finally { + els.confirmCancel.disabled = false; + els.confirmCancel.textContent = "Yes, Cancel event"; + } +} + +els.cancelBtn.addEventListener("click", openModal); + +els.goBackBtn.addEventListener("click", () => { + window.history.back(); +}); + +[els.closeModal, els.cancelModal].forEach((btn) => { + btn.addEventListener("click", hideModal); +}); + +els.confirmCancel.addEventListener("click", cancelEvent); + +els.logoutBtn.addEventListener("click", () => { + logout(ROUTES.organizerLogin); +}); + +document.addEventListener("DOMContentLoaded", async () => { + await requireRole("organizer", ROUTES.organizerLogin); + + if (!eventId) { + alert("Invalid event"); + window.location.href = ROUTES.eventListing; + return; + } +}); \ No newline at end of file diff --git a/Organizer/events/create-event.css b/Organizer/events/create-event.css new file mode 100644 index 0000000..58ae06a --- /dev/null +++ b/Organizer/events/create-event.css @@ -0,0 +1,111 @@ + +.page-title { + font-size: 2rem; + font-weight: 700; + color: #223f6b; +} + +.page-subtitle { + color: #6b7280; + font-size: 0.9rem; + margin-top: 0.3rem; +} + +.topbar-actions { + display: flex; + align-items: center; + gap: 1rem; +} + +.create-event-btn { + background: #223f6b; + color: #fff; + text-decoration: none; + padding: 0.95rem 1.4rem; + border-radius: 0.75rem; + font-weight: 600; + cursor: pointer; +} + +.icon-btn { + width: 2.6rem; + height: 2.6rem; + border: none; + border-radius: 50%; + background: transparent; + color: #223f6b; + font-size: 1.35rem; + cursor: pointer; +} + +.profile-mini img { + width: 2.5rem; + height: 2.5rem; + border-radius: 50%; + object-fit: cover; +} + + +.form-section { + padding: 2rem; + max-width: 600px; +} + +.image-upload { + border: 2px dashed #1f80ea; + padding: 2rem; + text-align: center; + margin-bottom: 1.5rem; + cursor: pointer; +} + +input, textarea { + width: 100%; + padding: 0.7rem; + margin: 0.5rem 0 1rem; + border-radius: 6px; + border: 1px solid #ccc; +} + +.grid-2 { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; +} + +.tags-input { + display: flex; + gap: 5px; +} + +#rolesList span { + background: #1f80ea; + color: white; + padding: 5px 10px; + margin: 5px; + display: inline-block; + border-radius: 5px; +} + +.actions { + display: flex; + justify-content: space-between; + margin-top: 20px; +} + +.actions button { + padding: 10px 20px; + border: none; + border-radius: 6px; + cursor: pointer; +} + +.actions button:first-child { + background: transparent; + border: 1px solid #1f80ea; +} + +.actions button:last-child { + background: #1f3b63; + color: white; +} diff --git a/Organizer/events/create-event.html b/Organizer/events/create-event.html new file mode 100644 index 0000000..cc1751a --- /dev/null +++ b/Organizer/events/create-event.html @@ -0,0 +1,167 @@ + + + + + + Create Event + + + + + + + + + + + + +
+ + + + + + +
+ +
+
+

Create Event

+
+ +
+ Create Event + +
+ Organizer Avatar +
+
+
+
+ + +
+ + +

Upload Event Image

+
+ + +
+ + + + + + + + +
+ + +
+
+ + + + + + + +
+
+ + +
+ +
+ + +
+
+ +
+
+ + +
+ +
+ + +
+
+ + + + + + + + + +
+ + +
+ +

+ +
+ +
+ +
+
+ + + + + + diff --git a/Organizer/events/create-event.js b/Organizer/events/create-event.js new file mode 100644 index 0000000..a6155f3 --- /dev/null +++ b/Organizer/events/create-event.js @@ -0,0 +1,279 @@ +// import { apiRequest } from "../../assets/js/api.js"; +// import { ROUTES } from "../../assets/js/config.js"; +// import { logout } from "../../assets/js/logout.js"; + +// const roles = []; +// let imageUrl = ""; + +// const els = { +// form: document.getElementById("eventForm"), +// roleInput: document.getElementById("roleInput"), +// addRole: document.getElementById("addRole"), +// rolesList: document.getElementById("rolesList"), +// imageInput: document.getElementById("imageInput"), +// imageBox: document.getElementById("imageBox"), +// formMsg: document.getElementById("formMsg"), +// saveDraft: document.getElementById("saveDraft"), +// logoutBtn: document.getElementById("logoutBtn") +// }; + +// /* IMAGE */ +// els.imageBox.addEventListener("click", () => els.imageInput.click()); + +// els.imageInput.addEventListener("change", async (e) => { +// const file = e.target.files[0]; +// if (!file) return; + +// // TEMP: Replace with Cloudinary later +// imageUrl = URL.createObjectURL(file); +// els.imageBox.innerHTML = ``; +// }); + +// /* ROLES */ +// els.addRole.addEventListener("click", () => { +// const val = els.roleInput.value.trim(); +// if (!val) return; + +// roles.push(val); +// els.roleInput.value = ""; +// renderRoles(); +// }); + +// function renderRoles() { +// els.rolesList.innerHTML = roles.map(r => `${r}`).join(""); +// } + +// /* CREATE EVENT */ +// async function createEvent(status = "draft") { +// const payload = { +// name: document.getElementById("name").value, +// category: document.getElementById("category").value, +// description: document.getElementById("description").value, +// location: { +// venue: document.getElementById("venue").value, +// city: document.getElementById("city").value +// }, +// image: imageUrl, +// date: document.getElementById("date").value, +// startTime: document.getElementById("startTime").value, +// endTime: document.getElementById("endTime").value, +// volunteerSlots: Number(document.getElementById("slots").value), +// roles, +// certificateEnabled: document.getElementById("certificateEnabled").checked, +// requirements: document +// .getElementById("requirements") +// .value.split("\n") +// }; + +// try { +// const res = await apiRequest("/events", { +// method: "POST", +// body: JSON.stringify(payload) +// }); + +// const eventId = res._id || res.id; + +// if (status === "published") { +// await apiRequest(`/events/${eventId}/status`, { +// method: "PATCH", +// body: JSON.stringify({ status: "published" }) +// }); +// } + +// els.formMsg.textContent = "Event created successfully!"; +// window.location.href = ROUTES.organizerDashboard; + +// } catch (err) { +// els.formMsg.textContent = err.message; +// } +// } + +// /* ACTIONS */ +// els.form.addEventListener("submit", (e) => { +// e.preventDefault(); +// createEvent("published"); +// }); + +// els.saveDraft.addEventListener("click", () => createEvent("draft")); + +// /* LOGOUT */ +// els.logoutBtn.addEventListener("click", () => { +// logout(ROUTES.organizerLogin); +// }); + + + + + + + + + + + +import { apiRequest } from "../../assets/js/api.js"; +import { ROUTES } from "../../assets/js/config.js"; +import { logout } from "../../assets/js/logout.js"; + +const roles = []; +let imageUrl = ""; + +const els = { + form: document.getElementById("eventForm"), + roleInput: document.getElementById("roleInput"), + addRole: document.getElementById("addRole"), + rolesList: document.getElementById("rolesList"), + imageInput: document.getElementById("imageInput"), + imageBox: document.getElementById("imageBox"), + formMsg: document.getElementById("formMsg"), + saveDraft: document.getElementById("saveDraft"), + logoutBtn: document.getElementById("logoutBtn") +}; + +/* IMAGE */ +els.imageBox?.addEventListener("click", () => els.imageInput?.click()); + +els.imageInput?.addEventListener("change", async (e) => { + const file = e.target.files?.[0]; + if (!file) return; + + // Temporary local preview + imageUrl = URL.createObjectURL(file); + els.imageBox.innerHTML = `Event image preview`; +}); + +/* ROLES */ +els.addRole?.addEventListener("click", () => { + const val = els.roleInput.value.trim(); + if (!val) return; + + if (!roles.includes(val)) { + roles.push(val); + } + + els.roleInput.value = ""; + renderRoles(); +}); + +function renderRoles() { + els.rolesList.innerHTML = roles + .map((role, index) => ` + + ${role} + + + `) + .join(""); + + document.querySelectorAll(".remove-role-btn").forEach((button) => { + button.addEventListener("click", () => { + const index = Number(button.dataset.index); + roles.splice(index, 1); + renderRoles(); + }); + }); +} + +function setMessage(message, type = "") { + if (!els.formMsg) return; + els.formMsg.textContent = message; + els.formMsg.className = "form-message"; + if (type) { + els.formMsg.classList.add(type); + } +} + +function getEventPayload() { + return { + name: document.getElementById("name")?.value.trim(), + category: document.getElementById("category")?.value.trim(), + description: document.getElementById("description")?.value.trim(), + location: { + venue: document.getElementById("venue")?.value.trim(), + city: document.getElementById("city")?.value.trim() + }, + image: imageUrl, + date: document.getElementById("date")?.value, + startTime: document.getElementById("startTime")?.value.trim(), + endTime: document.getElementById("endTime")?.value.trim(), + volunteerSlots: Number(document.getElementById("slots")?.value || 0), + roles, + certificateEnabled: document.getElementById("certificateEnabled")?.checked || false, + requirements: document + .getElementById("requirements") + ?.value.split("\n") + .map((item) => item.trim()) + .filter(Boolean) || [] + }; +} + +function extractEventId(res) { + return ( + res?._id || + res?.id || + res?.event?._id || + res?.event?.id || + res?.data?._id || + res?.data?.id || + "" + ); +} + +/* CREATE EVENT */ +async function createEvent(status = "draft") { + const payload = getEventPayload(); + + try { + setMessage(status === "published" ? "Creating and publishing event..." : "Saving draft..."); + + const res = await apiRequest("/events", { + method: "POST", + body: JSON.stringify(payload) + }); + + console.log("Create event response:", res); + + const eventId = extractEventId(res); + + if (!eventId) { + throw new Error("Event created but no event ID was returned by the backend."); + } + + if (status === "published") { + await apiRequest(`/events/${eventId}/status`, { + method: "PATCH", + body: JSON.stringify({ status: "published" }) + }); + } + + setMessage( + status === "published" + ? "Event published successfully!" + : "Draft saved successfully!", + "success" + ); + + setTimeout(() => { + window.location.href = ROUTES.organizerDashboard; + }, 800); + } catch (err) { + console.error("Create/publish event error:", err); + setMessage(err.message || "Something went wrong.", "error"); + } +} + +/* ACTIONS */ +els.form?.addEventListener("submit", (e) => { + e.preventDefault(); + createEvent("published"); +}); + +els.saveDraft?.addEventListener("click", (e) => { + e.preventDefault(); + createEvent("draft"); +}); + +/* LOGOUT */ +els.logoutBtn?.addEventListener("click", () => { + logout(ROUTES.organizerLogin); +}); \ No newline at end of file diff --git a/Organizer/events/event-details.css b/Organizer/events/event-details.css new file mode 100644 index 0000000..ad6b3f4 --- /dev/null +++ b/Organizer/events/event-details.css @@ -0,0 +1,130 @@ + +.top-actions { + display: flex; + align-items: center; + gap: 1rem; +} + +.btn-primary { + background: #223f6b; + color: #fff; + text-decoration: none; + padding: 0.95rem 1.4rem; + border-radius: 0.75rem; + font-weight: 600; +} + + +.btn-danger { + background: #dc2626; + color: #fff; + text-decoration: none; + padding: 0.95rem 1.4rem; + border-radius: 0.75rem; + font-weight: 600; + cursor: pointer; +} + +.icon-btn { + width: 2.6rem; + height: 2.6rem; + border: none; + border-radius: 50%; + background: transparent; + color: #223f6b; + font-size: 1.35rem; + cursor: pointer; +} + + +.profile-mini img { + width: 2.5rem; + height: 2.5rem; + border-radius: 50%; + object-fit: cover; +} + +.event-details { + padding: 2rem; +} + +.event-image { + position: relative; + margin: 1rem 0; +} + +.event-image img { + width: 100%; + border-radius: 10px; +} + +.status-badge { + position: absolute; + top: 15px; + right: 15px; + padding: 6px 14px; + border-radius: 6px; + color: #fff; + font-size: 0.85rem; +} + +.status-published { background: green; } +.status-draft { background: gray; } +.status-cancelled { background: red; } + +.description-box { + border: 1px solid #ccc; + padding: 1rem; + border-radius: 8px; + margin: 1rem 0; +} + +.event-meta { + display: flex; + gap: 2rem; + margin: 1rem 0; +} + +.requirements ul { + padding-left: 1.5rem; +} + +.stats { + display: flex; + gap: 2rem; + margin: 2rem 0; +} + +.stat { + background: #f3f5f9; + padding: 1rem; + border-radius: 8px; + text-align: center; +} + +.table-wrapper { + margin-top: 1.5rem; +} + +table { + width: 100%; + border-collapse: collapse; +} + +td, th { + padding: 0.8rem; + border-bottom: 1px solid #ddd; +} + +.row-actions { + text-align: center; +} + +.row-actions a, +.row-actions button { + color: #223f6b; + background: transparent; + border: none; + cursor: pointer; + font-size: 1.2rem; +} \ No newline at end of file diff --git a/Organizer/events/event-details.html b/Organizer/events/event-details.html new file mode 100644 index 0000000..c2b7b4c --- /dev/null +++ b/Organizer/events/event-details.html @@ -0,0 +1,151 @@ + + + + + + Event Details + + + + + + + + + + + +
+ + + + + +
+ + +
+

Events

+ +
+ + + + +
+ Organizer Avatar +
+
+ +
+ + +
+ +

Loading...

+ +
+ Event Image + Status +
+ +

Events Description

+
+ + +
+ + + +
+ + +
+

Volunteer Requirements

+
    +
    + + +
    +
    +

    0

    +

    Total Slots

    +
    +
    +

    0

    +

    Registered

    +
    +
    +

    0

    +

    Remaining

    +
    +
    + + +
    + + + + + + + + + + + + + +
    NameEmailDateStatus
    Loading...
    +
    + +
    +
    +
    + + + + + diff --git a/Organizer/events/event-details.js b/Organizer/events/event-details.js new file mode 100644 index 0000000..c318b56 --- /dev/null +++ b/Organizer/events/event-details.js @@ -0,0 +1,113 @@ +import { apiRequest } from "../../assets/js/api.js"; +import { requireRole } from "../../assets/js/auth.js"; +import { logout } from "../../assets/js/logout.js"; + +const els = { + name: document.getElementById("eventName"), + image: document.getElementById("eventImage"), + description: document.getElementById("eventDescription"), + time: document.getElementById("eventTime"), + date: document.getElementById("eventDate"), + location: document.getElementById("eventLocation"), + requirements: document.getElementById("requirementsList"), + totalSlots: document.getElementById("totalSlots"), + registered: document.getElementById("registered"), + remaining: document.getElementById("remaining"), + table: document.getElementById("volunteerTable"), + statusBadge: document.getElementById("statusBadge"), + cancelBtn: document.getElementById("cancelBtn"), + editBtn: document.getElementById("editBtn"), + logoutBtn: document.getElementById("logoutBtn") +}; + +const eventId = new URLSearchParams(window.location.search).get("id"); + +let eventData = null; + +function setStatus(status) { + els.statusBadge.textContent = status; + els.statusBadge.className = `status-badge status-${status}`; +} + +async function loadEvent() { + const events = await apiRequest("/events"); + eventData = events.find(e => e._id === eventId); + + if (!eventData) { + els.name.textContent = "Event not found"; + return; + } + + els.name.textContent = eventData.name; + els.image.src = eventData.image; + els.description.textContent = eventData.description; + + els.time.textContent = `${eventData.startTime} - ${eventData.endTime}`; + els.date.textContent = eventData.date; + els.location.textContent = eventData.location?.venue + ", " + eventData.location?.city; + + setStatus(eventData.status); + + // Requirements + els.requirements.innerHTML = eventData.requirements + .map(r => `
  • ${r}
  • `) + .join(""); + + // Stats + els.totalSlots.textContent = eventData.volunteerSlots; + + await loadVolunteers(); +} + +async function loadVolunteers() { + const data = await apiRequest(`/applications/events/${eventId}/registrations`); + + const volunteers = Array.isArray(data) ? data : data.data || []; + + els.registered.textContent = volunteers.length; + els.remaining.textContent = eventData.volunteerSlots - volunteers.length; + + if (!volunteers.length) { + els.table.innerHTML = `No volunteers yet`; + return; + } + + els.table.innerHTML = volunteers.map(v => ` + + ${v.user?.fullName || "Unknown"} + ${v.user?.email || "—"} + ${new Date(v.createdAt).toDateString()} + Confirmed + + `).join(""); +} + +// Cancel event +els.cancelBtn.addEventListener("click", async () => { + if (!confirm("Cancel this event?")) return; + + await apiRequest(`/events/${eventId}/cancel`, { + method: "PATCH", + body: JSON.stringify({ reason: "Cancelled by organizer" }) + }); + + alert("Event cancelled"); + location.reload(); +}); + +// Edit +els.editBtn.addEventListener("click", () => { + window.location.href = `create-event.html?id=${eventId}`; +}); + +// Logout +els.logoutBtn.addEventListener("click", () => { + logout("../login/organizer-login.html/"); + +}); + +// Init +document.addEventListener("DOMContentLoaded", async () => { + await requireRole("organizer", "../login/organizer-login.html"); + await loadEvent(); +}); diff --git a/Organizer/events/event-listing.css b/Organizer/events/event-listing.css new file mode 100644 index 0000000..654fd13 --- /dev/null +++ b/Organizer/events/event-listing.css @@ -0,0 +1,249 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: "Poppins", sans-serif; +} + +body { + background: #f4f7fb; + color: #1f2f46; +} + +.dashboard-layout { + display: flex; + min-height: 100vh; +} + +.sidebar { + width: 15rem; + background: #1f3b63; + color: #fff; + padding: 1.5rem 1rem; + flex-shrink: 0; +} + +.sidebar-logo { + text-align: center; + margin-bottom: 2rem; +} + +.sidebar-logo img { + width: 6rem; +} + +.sidebar-menu { + list-style: none; + display: flex; + flex-direction: column; + gap: 0.55rem; +} + +.sidebar-menu li a, +.sidebar-logout { + width: 100%; + display: flex; + align-items: center; + gap: 0.85rem; + color: #fff; + text-decoration: none; + padding: 0.95rem 1rem; + border-radius: 0.65rem; + font-size: 0.98rem; + border: none; + background: transparent; + cursor: pointer; +} + +.sidebar-menu li.active a, +.sidebar-menu li a:hover { + background: #1f80ea; +} + +.sidebar-logout:hover { + background: #dc2626; +} + +.main-content { + flex: 1; +} + +.topbar { + background: #fff; + padding: 1.5rem 2rem; + display: flex; + justify-content: space-between; + align-items: flex-start; +} + +.page-title { + font-size: 2rem; + font-weight: 700; + color: #223f6b; +} + +.page-subtitle { + color: #6b7280; + font-size: 0.9rem; + margin-top: 0.3rem; +} + +.topbar-actions { + display: flex; + align-items: center; + gap: 1rem; +} + +.create-event-btn { + background: #223f6b; + color: #fff; + text-decoration: none; + padding: 0.95rem 1.4rem; + border-radius: 0.75rem; + font-weight: 600; +} + +.icon-btn { + width: 2.6rem; + height: 2.6rem; + border: none; + border-radius: 50%; + background: transparent; + color: #223f6b; + font-size: 1.35rem; + cursor: pointer; +} + +.profile-mini img { + width: 2.5rem; + height: 2.5rem; + border-radius: 50%; + object-fit: cover; +} + +.events-section { + padding: 2rem; +} + +.filter-tabs { + display: flex; + gap: 2rem; + margin-bottom: 1.5rem; + flex-wrap: wrap; +} + +.filter-btn { + background: transparent; + border: none; + color: #111827; + font-size: 1rem; + cursor: pointer; + padding-bottom: 0.4rem; +} + +.filter-btn.active { + color: #1f80ea; + font-weight: 600; +} + +.table-wrapper { + background: #fff; + border-radius: 0.85rem; + overflow: hidden; +} + +table { + width: 100%; + border-collapse: collapse; +} + +thead { + background: #eef2f6; +} + +th, +td { + padding: 1rem; + text-align: left; + font-size: 0.92rem; +} + +th { + color: #35527a; + font-weight: 600; +} + +tbody tr { + border-bottom: 0.08rem solid #d7dde6; +} + +tbody tr:last-child { + border-bottom: none; +} + +.status-badge { + display: inline-block; + min-width: 7rem; + text-align: center; + padding: 0.62rem 1rem; + border-radius: 0.45rem; + color: #fff; + font-size: 0.88rem; + font-weight: 600; +} + +.status-upcoming { + background: #f5ae42; +} + +.status-published { + background: #26a65b; +} + +.status-draft { + background: #6f809b; +} + +.status-completed { + background: #1f80ea; +} + +.status-cancelled { + background: #dc2626; +} + +.row-actions { + text-align: center; +} + +.row-actions a, +.row-actions button { + color: #223f6b; + background: transparent; + border: none; + cursor: pointer; + font-size: 1.2rem; +} + +@media (max-width: 52rem) { + .dashboard-layout { + flex-direction: column; + } + + .sidebar { + width: 100%; + } + + .topbar { + flex-direction: column; + gap: 1rem; + } + + .table-wrapper { + overflow-x: auto; + } + + table { + min-width: 50rem; + } +} diff --git a/Organizer/events/event-listing.html b/Organizer/events/event-listing.html new file mode 100644 index 0000000..3760f04 --- /dev/null +++ b/Organizer/events/event-listing.html @@ -0,0 +1,110 @@ + + + + + + AIDLoop Organizer Events + + + + + + + + +
    + + +
    +
    +
    +

    Events

    +

    Browse and Manage all your events

    +
    + +
    + Create Event + +
    + Organizer Avatar +
    +
    +
    + +
    +
    + + + + +
    + +
    + + + + + + + + + + + + + + + + + +
    Events NameLocationDateVolunteersStatus
    Loading events...
    +
    +
    +
    +
    + + + + diff --git a/Organizer/events/event-listing.js b/Organizer/events/event-listing.js new file mode 100644 index 0000000..5946d06 --- /dev/null +++ b/Organizer/events/event-listing.js @@ -0,0 +1,46 @@ +import { apiRequest, normalizeArray } from "../../assets/js/api.js"; +import { requireOrganizer } from "../../assets/js/auth.js"; +import { logout } from "../../assets/js/logout.js"; +import { ROUTES } from "../../assets/js/config.js"; +import { formatDate, getLocationText } from "../../assets/js/utils.js"; + +const table = document.getElementById("eventsTable"); +document.getElementById("logoutBtn").addEventListener("click", () => logout(ROUTES.home)); + +function statusOf(event) { + const raw = String(event.status || "").toLowerCase(); + if (raw === "cancelled" || raw === "canceled") return "cancelled"; + if (raw === "draft") return "draft"; + if (raw === "published" && event.date && new Date(event.date) < new Date()) return "completed"; + return raw || "published"; +} + +document.addEventListener("DOMContentLoaded", async () => { + const organizer = await requireOrganizer(); + if (!organizer) return; + + try { + const payload = await apiRequest("/events"); + const events = normalizeArray(payload, ["events"]); + const organizerId = String(organizer._id || organizer.id || ""); + const own = events.filter((event) => { + if (typeof event.organizer === "object" && event.organizer) { + return String(event.organizer._id || event.organizer.id || "") === organizerId; + } + return String(event.organizerId || "") === organizerId; + }); + + table.innerHTML = own.map((event) => ` + + ${event.name || "Untitled Event"} + ${getLocationText(event)} + ${formatDate(event.date, "long")} + ${event.filledSlots ?? event.registrationsCount ?? 0}/${event.volunteerSlots ?? 0} + ${statusOf(event)} + Details + + `).join("") || `No events found.`; + } catch { + table.innerHTML = `Failed to load events.`; + } +}); diff --git a/Organizer/login/organizer-login.css b/Organizer/login/organizer-login.css new file mode 100644 index 0000000..3d4b3b7 --- /dev/null +++ b/Organizer/login/organizer-login.css @@ -0,0 +1,190 @@ +Reset +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: "Poppins", sans-serif; +} + +/* Page */ +body { + background: #f5f7fa; + display: flex; + align-items: center; + justify-content: center; + min-height: 100vh; +} + +/* Container */ +.login-page { + width: 100%; + display: flex; + justify-content: center; + padding: 20px; +} + +/* Card */ +.login-card { + background: #ffffff; + padding: 40px 35px; + border-radius: 18px; + width: 100%; + max-width: 500px; + text-align: center; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.06); +} + +/* Logo */ +.logo-wrap { + margin-bottom: 20px; +} + +.logo { + width: 90px; + margin-bottom: 10px; +} + +.logo-wrap p { + font-size: 14px; + color: #6b7280; +} + +/* Title */ +.login-card h1 { + font-size: 24px; + font-weight: 600; + margin-bottom: 10px; + color: #111827; +} + +.subtitle { + font-size: 14px; + color: #6b7280; + margin-bottom: 25px; + line-height: 1.5; +} + +/* Form */ +.form-group { + text-align: left; + margin-bottom: 20px; +} + +.form-group label { + font-size: 14px; + font-weight: 500; + margin-bottom: 6px; + display: block; + color: #374151; +} + +/* Inputs */ +.form-group input { + width: 100%; + padding: 14px 16px; + border-radius: 10px; + border: 1px solid #d1d5db; + font-size: 14px; + outline: none; + transition: 0.2s ease; +} + +.form-group input:focus { + border-color: #2563eb; + box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.1); +} + +/* Password */ +.password-wrap { + position: relative; +} + +.password-wrap input { + padding-right: 45px; +} + +.password-wrap button { + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + background: transparent; + border: none; + cursor: pointer; + color: #6b7280; + font-size: 16px; +} + +/* Button */ +.btn-primary { + width: 100%; + background: #1f3b63; + color: #fff; + padding: 15px; + border: none; + border-radius: 12px; + font-size: 15px; + font-weight: 500; + cursor: pointer; + margin-top: 10px; + transition: 0.3s; +} + +.btn-primary:hover { + background: #173052; +} + +/* Links */ +.links { + margin-top: 18px; + font-size: 13px; + color: #6b7280; + display: flex; + justify-content: center; + gap: 10px; +} + +.links a { + text-decoration: none; + color: #2563eb; + font-weight: 500; +} + +.links a:hover { + text-decoration: underline; +} + +/* Bottom text */ +.register-text { + margin-top: 15px; + font-size: 13px; + color: #6b7280; +} + +.register-text a { + color: #2563eb; + font-weight: 500; + text-decoration: none; +} + +.register-text a:hover { + text-decoration: underline; +} + +/* Errors */ +.error-message { + color: #dc2626; + font-size: 12px; + margin-top: 4px; +} + +/* Responsive */ +@media (max-width: 480px) { + .login-card { + padding: 30px 20px; + } + + .login-card h1 { + font-size: 20px; + } +} diff --git a/Organizer/login/organizer-login.html b/Organizer/login/organizer-login.html new file mode 100644 index 0000000..56d1174 --- /dev/null +++ b/Organizer/login/organizer-login.html @@ -0,0 +1,81 @@ + + + + + + AIDLoop Organizer Login + + + + + + + + + + +
    + +
    + + + + diff --git a/Organizer/login/organizer-login.js b/Organizer/login/organizer-login.js new file mode 100644 index 0000000..59c2685 --- /dev/null +++ b/Organizer/login/organizer-login.js @@ -0,0 +1,92 @@ +import { apiRequest } from "../../assets/js/api.js"; +import { ROUTES } from "../../assets/js/config.js"; + +const els = { + form: document.getElementById("loginForm"), + email: document.getElementById("email"), + password: document.getElementById("password"), + togglePassword: document.getElementById("togglePassword"), + loginBtn: document.getElementById("loginBtn"), + + emailError: document.getElementById("emailError"), + passwordError: document.getElementById("passwordError"), + formError: document.getElementById("formError"), + formSuccess: document.getElementById("formSuccess") +}; + +function clearMessages() { + els.emailError.textContent = ""; + els.passwordError.textContent = ""; + els.formError.textContent = ""; + els.formSuccess.textContent = ""; +} + +function validateEmail(email) { + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); +} + +function setLoading(isLoading) { + els.loginBtn.disabled = isLoading; + els.loginBtn.textContent = isLoading ? "Logging in..." : "Log in"; +} + +/* Toggle password */ +els.togglePassword.addEventListener("click", () => { + const isPassword = els.password.type === "password"; + els.password.type = isPassword ? "text" : "password"; + + els.togglePassword.innerHTML = isPassword + ? '' + : ''; +}); + +/* Submit */ +els.form.addEventListener("submit", async (e) => { + e.preventDefault(); + clearMessages(); + + const email = els.email.value.trim(); + const password = els.password.value.trim(); + + let valid = true; + + if (!email) { + els.emailError.textContent = "Email is required"; + valid = false; + } else if (!validateEmail(email)) { + els.emailError.textContent = "Invalid email format"; + valid = false; + } + + if (!password) { + els.passwordError.textContent = "Password is required"; + valid = false; + } + + if (!valid) return; + + try { + setLoading(true); + + const response = await apiRequest("/auth/webLogin", { + method: "POST", + body: JSON.stringify({ email, password }) + }); + + els.formSuccess.textContent = response.message || "Login successful"; + + /* Optional: role check */ + if (response?.user?.role !== "organizer") { + throw new Error("Not an organizer account"); + } + + setTimeout(() => { + window.location.href = ROUTES.organizerDashboard; + }, 800); + + } catch (error) { + els.formError.textContent = error.message || "Login failed"; + } finally { + setLoading(false); + } +}); diff --git a/Organizer/profile/organizer-profile.css b/Organizer/profile/organizer-profile.css new file mode 100644 index 0000000..9288c7a --- /dev/null +++ b/Organizer/profile/organizer-profile.css @@ -0,0 +1,385 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: "Poppins", sans-serif; +} + +body { + background: #f4f7fb; + color: #1f2f46; +} + +.dashboard-layout { + display: flex; + min-height: 100vh; +} + +.sidebar { + width: 15rem; + background: #1f3b63; + color: #ffffff; + padding: 1.5rem 1rem; + flex-shrink: 0; +} + +.sidebar-logo { + text-align: center; + margin-bottom: 2rem; +} + +.sidebar-logo img { + width: 5.5rem; + max-width: 100%; +} + +.sidebar-menu { + list-style: none; + display: flex; + flex-direction: column; + gap: 0.55rem; +} + +.sidebar-menu li a, +.sidebar-logout { + width: 100%; + display: flex; + align-items: center; + gap: 0.85rem; + color: #ffffff; + text-decoration: none; + padding: 0.95rem 1rem; + border-radius: 0.65rem; + font-size: 0.96rem; + border: none; + background: transparent; + cursor: pointer; +} + +.sidebar-menu li.active a, +.sidebar-menu li a:hover { + background: #1f80ea; +} + +.sidebar-logout:hover { + background: #dc2626; +} + +.main-content { + flex: 1; + display: flex; + flex-direction: column; +} + +.topbar { + background: #ffffff; + padding: 2rem 2.2rem 1.2rem; + display: flex; + justify-content: space-between; + align-items: flex-start; +} + +.page-title { + font-size: 2rem; + font-weight: 700; + color: #223f6b; +} + +.page-subtitle { + margin-top: 0.3rem; + font-size: 0.9rem; + color: #6b7280; +} + +.topbar-right { + display: flex; + align-items: center; + gap: 0.9rem; +} + +.edit-profile-btn { + border: none; + background: #223f6b; + color: #ffffff; + padding: 0.95rem 1.45rem; + border-radius: 0.7rem; + font-size: 0.95rem; + font-weight: 600; + cursor: pointer; +} + +.icon-btn { + width: 2.7rem; + height: 2.7rem; + border: none; + border-radius: 50%; + background: transparent; + color: #223f6b; + font-size: 1.35rem; + cursor: pointer; +} + +.page-content { + padding: 2.2rem; +} + +.profile-card, +.about-card { + background: #ffffff; + border-radius: 1rem; + padding: 1.6rem; + margin-bottom: 2rem; +} + +.profile-card { + max-width: 52rem; +} + +.profile-main { + display: flex; + align-items: flex-start; + gap: 1.2rem; + margin-bottom: 1.5rem; +} + +.profile-avatar { + width: 3.7rem; + height: 3.7rem; + border-radius: 0.7rem; + background: #223f6b; + color: #ffffff; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.45rem; + font-weight: 700; + flex-shrink: 0; + text-transform: uppercase; +} + +.profile-info h2 { + font-size: 2rem; + color: #223f6b; + font-weight: 700; +} + +.org-type-line { + margin-top: 0.25rem; + color: #6b7280; + font-size: 0.95rem; + display: flex; + gap: 0.4rem; + align-items: center; + flex-wrap: wrap; +} + +.dot { + color: #9ca3af; +} + +.verified-badge { + margin-top: 1rem; + display: inline-flex; + align-items: center; + gap: 0.45rem; + background: #edf8ef; + color: #2e7d32; + border: 0.08rem solid #b9dfbf; + padding: 0.45rem 0.8rem; + border-radius: 999rem; + font-size: 0.82rem; + font-weight: 600; +} + +.verified-dot { + width: 0.45rem; + height: 0.45rem; + border-radius: 50%; + background: #35b24a; +} + +.contact-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; +} + +.info-chip { + background: #f4f6fa; + border-radius: 0.7rem; + padding: 0.95rem 1rem; + display: flex; + align-items: center; + gap: 0.65rem; +} + +.info-chip i { + color: #223f6b; + width: 1rem; + flex-shrink: 0; +} + +.info-chip input { + width: 100%; + border: none; + background: transparent; + outline: none; + color: #1f2f46; + font-size: 0.93rem; +} + +.about-card { + max-width: 53rem; +} + +.about-card h3 { + font-size: 1.8rem; + color: #223f6b; + margin-bottom: 1rem; +} + +.about-box { + background: #d9e9fb; + border-radius: 0.9rem; + padding: 1rem; +} + +.about-box textarea { + width: 100%; + min-height: 6rem; + border: none; + background: transparent; + resize: vertical; + outline: none; + color: #1f2f46; + font-size: 0.95rem; + line-height: 1.7; +} + +.stats-row { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 1.8rem; + max-width: 54rem; +} + +.mini-stat-card { + background: #ffffff; + border-radius: 0.9rem; + padding: 1.15rem; + display: flex; + align-items: center; + gap: 1rem; + box-shadow: 0 0.15rem 0.4rem rgba(0, 0, 0, 0.04); +} + +.mini-stat-icon { + width: 3rem; + height: 3rem; + border-radius: 0.6rem; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.2rem; + flex-shrink: 0; +} + +.mini-stat-icon.blue { + background: #dcecff; + color: #1f80ea; +} + +.mini-stat-icon.green { + background: #dff2e4; + color: #2e8b57; +} + +.mini-stat-icon.gold { + background: #fff0d8; + color: #d29a1f; +} + +.mini-stat-text { + display: flex; + flex-direction: column; + gap: 0.35rem; +} + +.mini-stat-label { + font-size: 0.72rem; + color: #8a94a6; + font-weight: 600; +} + +.mini-stat-text h4 { + font-size: 1.8rem; + color: #223f6b; + font-weight: 700; +} + +.mini-badge { + display: inline-block; + width: fit-content; + padding: 0.35rem 0.55rem; + border-radius: 0.45rem; + font-size: 0.72rem; + font-weight: 600; +} + +.mini-badge.blue { + background: #dcecff; + color: #1f80ea; +} + +.mini-badge.green { + background: #dff2e4; + color: #2e8b57; +} + +.mini-badge.gold { + background: #fff0d8; + color: #d29a1f; +} + +.profile-message { + margin-top: 1rem; + font-size: 0.92rem; + font-weight: 500; +} + +.profile-message.success { + color: #15803d; +} + +.profile-message.error { + color: #dc2626; +} + +@media (max-width: 68rem) { + .stats-row { + grid-template-columns: 1fr; + } +} + +@media (max-width: 64rem) { + .dashboard-layout { + flex-direction: column; + } + + .sidebar { + width: 100%; + } + + .topbar { + flex-direction: column; + gap: 1rem; + } + + .contact-grid { + grid-template-columns: 1fr; + } + + .profile-main { + flex-direction: column; + } +} diff --git a/Organizer/profile/organizer-profile.html b/Organizer/profile/organizer-profile.html new file mode 100644 index 0000000..73cb2a2 --- /dev/null +++ b/Organizer/profile/organizer-profile.html @@ -0,0 +1,186 @@ + + + + + + AIDLoop Organization Profile + + + + + + + + + +
    + + +
    +
    +
    +

    Organization Profile

    +

    Manage your organization information.

    +
    + +
    + + + + + +
    +
    + +
    +
    +
    +
    AL
    + +
    +

    Loading...

    +

    + Organization + + Volunteer Management +

    + +
    + + Verified Org +
    +
    +
    + +
    +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    +
    +
    + +
    +

    About Organization

    +
    + +
    +
    + +
    +
    +
    + +
    +
    +

    TOTAL EVENTS CREATED

    +

    0

    + This month +
    +
    + +
    +
    + +
    +
    +

    TOTAL VOLUNTEERS

    +

    0

    + This month +
    +
    + +
    +
    + +
    +
    +

    CERTIFICATES ISSUED

    +

    0

    + This month +
    +
    +
    + +

    +
    +
    +
    + + + + diff --git a/Organizer/profile/organizer-profile.js b/Organizer/profile/organizer-profile.js new file mode 100644 index 0000000..f4d2f01 --- /dev/null +++ b/Organizer/profile/organizer-profile.js @@ -0,0 +1,196 @@ +import { apiRequest, normalizeArray } from "../../assets/js/api.js"; +import { requireRole } from ".../../assets/js/auth.js"; +import { logout } from "../../assets/js/logout.js"; +import { ROUTES } from "../../assets/js/config.js"; + +const els = { + orgName: document.getElementById("orgName"), + orgType: document.getElementById("orgType"), + orgCategory: document.getElementById("orgCategory"), + verificationText: document.getElementById("verificationText"), + profileAvatarBox: document.getElementById("profileAvatarBox"), + + email: document.getElementById("email"), + phoneNumber: document.getElementById("phoneNumber"), + website: document.getElementById("website"), + location: document.getElementById("location"), + description: document.getElementById("description"), + + totalEvents: document.getElementById("totalEvents"), + totalVolunteers: document.getElementById("totalVolunteers"), + certificatesIssued: document.getElementById("certificatesIssued"), + + eventsMonthText: document.getElementById("eventsMonthText"), + volunteersMonthText: document.getElementById("volunteersMonthText"), + certificatesMonthText: document.getElementById("certificatesMonthText"), + + editProfileBtn: document.getElementById("editProfileBtn"), + logoutBtn: document.getElementById("logoutBtn"), + profileMessage: document.getElementById("profileMessage") +}; + +let organizer = null; +let isEditing = false; + +function getInitials(name) { + return String(name || "AL") + .split(" ") + .slice(0, 2) + .map((part) => part.charAt(0)) + .join("") + .toUpperCase(); +} + +function getVerificationLabel(user) { + const status = String(user.status || "").toLowerCase(); + const approvalStatus = String(user.approvalStatus || "").toLowerCase(); + const isVerified = Boolean(user.isVerified); + + if ( + status === "verified" || + status === "approved" || + approvalStatus === "verified" || + approvalStatus === "approved" || + isVerified + ) { + return "Verified Org"; + } + + return "Pending Verification"; +} + +function getLocationText(user) { + if (typeof user.location === "string" && user.location.trim()) { + return user.location; + } + + if (user.location && typeof user.location === "object") { + return [ + user.location.venue, + user.location.city || user.location.state + ].filter(Boolean).join(", "); + } + + return user.city || user.state || "—"; +} + +function populateProfile(user) { + const name = user.fullName || user.name || user.organizationName || "Organization"; + + els.orgName.textContent = name; + els.orgType.textContent = user.organizationType || "Non-profit"; + els.orgCategory.textContent = user.category || "Volunteer Management"; + els.verificationText.textContent = getVerificationLabel(user); + els.profileAvatarBox.textContent = getInitials(name); + + els.email.value = user.email || ""; + els.phoneNumber.value = user.phoneNumber || user.phone || ""; + els.website.value = + user.website || + user.socialLink || + user.socialLinks?.[0] || + "—"; + els.location.value = getLocationText(user) || "—"; + els.description.value = + user.description || + user.bio || + "No organization description available."; +} + +function setEditable(editable) { + els.phoneNumber.readOnly = !editable; + els.website.readOnly = !editable; + els.location.readOnly = !editable; + els.description.readOnly = !editable; +} + +async function loadStats() { + try { + const eventsPayload = await apiRequest("/events"); + const events = normalizeArray(eventsPayload, ["events"]); + + const organizerId = String(organizer._id || organizer.id || ""); + + const ownEvents = events.filter((event) => { + if (typeof event.organizer === "object" && event.organizer) { + return String(event.organizer._id || event.organizer.id || "") === organizerId; + } + return String(event.organizerId || "") === organizerId; + }); + + const totalEvents = ownEvents.length; + + const totalVolunteers = ownEvents.reduce((sum, event) => { + return sum + ( + event.filledSlots ?? + event.registrationsCount ?? + event.registeredCount ?? + event.attendeesCount ?? + 0 + ); + }, 0); + + els.totalEvents.textContent = totalEvents; + els.totalVolunteers.textContent = totalVolunteers; + els.certificatesIssued.textContent = "0"; + + els.eventsMonthText.textContent = "This month"; + els.volunteersMonthText.textContent = "This month"; + els.certificatesMonthText.textContent = "This month"; + } catch { + els.totalEvents.textContent = "0"; + els.totalVolunteers.textContent = "0"; + els.certificatesIssued.textContent = "0"; + } +} + +async function saveProfile() { + const payload = { + phoneNumber: els.phoneNumber.value.trim(), + website: els.website.value.trim(), + location: els.location.value.trim(), + description: els.description.value.trim() + }; + + await apiRequest("/user/me", { + method: "PUT", + body: JSON.stringify(payload) + }); +} + +els.editProfileBtn.addEventListener("click", async () => { + els.profileMessage.textContent = ""; + els.profileMessage.className = "profile-message"; + + if (!isEditing) { + isEditing = true; + setEditable(true); + els.editProfileBtn.textContent = "Save Profile"; + return; + } + + try { + await saveProfile(); + isEditing = false; + setEditable(false); + els.editProfileBtn.textContent = "Edit Profile"; + els.profileMessage.textContent = "Profile updated successfully."; + els.profileMessage.classList.add("success"); + } catch (error) { + els.profileMessage.textContent = error.message || "Failed to update profile."; + els.profileMessage.classList.add("error"); + } +}); + +els.logoutBtn.addEventListener("click", () => { + logout(ROUTES.organizerLogin); +}); + +document.addEventListener("DOMContentLoaded", async () => { + organizer = await requireRole("organizer", ROUTES.organizerLogin); + if (!organizer) return; + + populateProfile(organizer); + setEditable(false); + await loadStats(); +}); diff --git a/Organizer/signup/organizer-signup.css b/Organizer/signup/organizer-signup.css new file mode 100644 index 0000000..03d14e4 --- /dev/null +++ b/Organizer/signup/organizer-signup.css @@ -0,0 +1,111 @@ +body { + background: #f5f7fa; + font-family: "Poppins", sans-serif; +} + +.signup-page { + display: flex; + justify-content: center; + padding: 40px 20px; +} + + +.signup-card { + background: #fff; + width: 100%; + max-width: 520px; + padding: 40px; + border-radius: 18px; + box-shadow: 0 10px 40px rgba(0,0,0,0.05); +} + + +.logo-wrap { + text-align: center; + margin-bottom: 20px; +} + +.logo { + width: 80px; +} + +.logo-wrap h2 { + margin-top: 10px; + font-size: 22px; +} + +.logo-wrap p { + font-size: 13px; + color: #6b7280; +} + + +.section-title { + background: #6b7a90; + color: #fff; + padding: 10px; + border-radius: 8px; + font-size: 13px; + margin: 20px 0 10px; + text-align: center; +} + + +.form-group { + margin-bottom: 15px; +} + +label { + font-size: 13px; + color: #374151; +} + +input, +textarea { + width: 100%; + padding: 12px; + border-radius: 8px; + border: 1px solid #d1d5db; + margin-top: 5px; +} + +textarea { + min-height: 80px; +} + +.row { + display: flex; + gap: 10px; +} + +.row input { + flex: 1; +} + + +.btn-primary { + width: 100%; + padding: 14px; + border-radius: 10px; + border: none; + background: #1f3b63; + color: #fff; + margin-top: 20px; + cursor: pointer; +} + +.login-link { + text-align: center; + margin-top: 15px; + font-size: 13px; +} + +.login-link a { + color: #2563eb; +} + + + + + + diff --git a/Organizer/signup/organizer-signup.html b/Organizer/signup/organizer-signup.html new file mode 100644 index 0000000..7bb090c --- /dev/null +++ b/Organizer/signup/organizer-signup.html @@ -0,0 +1,90 @@ + + + + + + AIDLoop Organizer Signup + + + + + + + + + + + +
    + +
    + + + + + + diff --git a/Organizer/signup/organizer-signup.js b/Organizer/signup/organizer-signup.js new file mode 100644 index 0000000..abc9a69 --- /dev/null +++ b/Organizer/signup/organizer-signup.js @@ -0,0 +1,80 @@ +import { apiRequest } from "../../assets/js/api.js"; +import { ROUTES } from "../../assets/js/config.js"; + +const els = { + form: document.getElementById("signupForm"), + name: document.getElementById("name"), + email: document.getElementById("email"), + password: document.getElementById("password"), + phone: document.getElementById("phone"), + state: document.getElementById("state"), + city: document.getElementById("city"), + description: document.getElementById("description"), + social: document.getElementById("social"), + btn: document.getElementById("signupBtn"), + error: document.getElementById("formError"), + success: document.getElementById("formSuccess") +}; + +function setLoading(isLoading) { + els.btn.disabled = isLoading; + els.btn.textContent = isLoading ? "Creating account..." : "Sign Up"; +} + +function validateEmail(email) { + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); +} + +els.form.addEventListener("submit", async (e) => { + e.preventDefault(); + + els.error.textContent = ""; + els.success.textContent = ""; + + const fullName = els.name.value.trim(); + const email = els.email.value.trim(); + const password = els.password.value.trim(); + + if (!fullName || !email || !password) { + els.error.textContent = "All required fields must be filled"; + return; + } + + if (!validateEmail(email)) { + els.error.textContent = "Invalid email address"; + return; + } + + if (password.length < 6) { + els.error.textContent = "Password must be at least 6 characters"; + return; + } + + try { + setLoading(true); + + const response = await apiRequest("/auth/register/web", { + method: "POST", + header: {"Content-Type": "application.json"}, + body: JSON.stringify({ + fullName, + email, + password + }) + }); + + els.success.textContent = + response.message || "Account created successfully. Check your email for verification."; + + sessionStorage.setItem("aidloop_pending_verification_email", email); + localStorage.setItem("aidloop_organizer_email", email); + + setTimeout(() => { + window.location.href = "../verify-email/verify-email.html"; + }, 1200); + } catch (error) { + els.error.textContent = error.message || "Signup failed"; + } finally { + setLoading(false); + } +}); diff --git a/Organizer/verify-email/verify-email.css b/Organizer/verify-email/verify-email.css new file mode 100644 index 0000000..16932d4 --- /dev/null +++ b/Organizer/verify-email/verify-email.css @@ -0,0 +1,123 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: "Poppins", sans-serif; +} + +body { + background: #f5f7fa; + min-height: 100vh; +} + +.verify-page { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 2rem; +} + +.verify-card { + width: 100%; + max-width: 40rem; + min-height: 46rem; + background: #ffffff; + padding: 4rem 2rem; + text-align: center; + box-shadow: 0 0.625rem 2.5rem rgba(0, 0, 0, 0.05); +} + +.icon-wrap { + display: flex; + justify-content: center; + margin-bottom: 1.5rem; +} + +.mail-box { + width: 7rem; + height: 7rem; + border-radius: 1rem; + background: #f5ae42; + display: flex; + align-items: center; + justify-content: center; +} + +.mail-box i { + font-size: 4rem; + color: #ffffff; +} + +.verify-card h1 { + font-size: 2rem; + color: #223f6b; + margin-bottom: 3rem; + font-weight: 700; +} + +.divider { + width: 100%; + height: 0.0625rem; + background: #cfd5dc; + margin-bottom: 1.5rem; +} + +.message { + max-width: 25rem; + margin: 0 auto 0.75rem; + font-size: 1rem; + line-height: 1.7; + color: #4b5563; +} + +.email-text { + margin-bottom: 1.5rem; + font-size: 0.95rem; + color: #223f6b; + font-weight: 600; +} + +.primary-btn { + margin-top: 2rem; + width: 100%; + max-width: 15rem; + border: none; + background: #223f6b; + color: #ffffff; + padding: 1rem 1.25rem; + border-radius: 0.75rem; + font-size: 1rem; + font-weight: 600; + cursor: pointer; +} + +.primary-btn:hover { + background: goldenrod; +} + +.primary-btn:disabled { + background: #dc2626; + cursor: not-allowed; +} + +@media (max-width: 40rem) { + .verify-card { + min-height: auto; + padding: 3rem 1.25rem; + } + + .verify-card h1 { + font-size: 1.7rem; + margin-bottom: 2rem; + } + + .mail-box { + width: 6rem; + height: 6rem; + } + + .mail-box i { + font-size: 3.2rem; + } +} diff --git a/Organizer/verify-email/verify-email.html b/Organizer/verify-email/verify-email.html new file mode 100644 index 0000000..b2a10a6 --- /dev/null +++ b/Organizer/verify-email/verify-email.html @@ -0,0 +1,44 @@ + + + + + + Verify Your Email + + + + + + + + + +
    +
    +
    +
    + +
    +
    + +

    Verify Your Email

    + +
    + +

    + We've sent a verification link to your email address. + Please check your inbox and click the link to continue. +

    + + + + + + + +
    +
    + + + + diff --git a/Organizer/verify-email/verify-email.js b/Organizer/verify-email/verify-email.js new file mode 100644 index 0000000..dd6306c --- /dev/null +++ b/Organizer/verify-email/verify-email.js @@ -0,0 +1,70 @@ +import { apiRequest } from "../../assets/js/api.js"; +import { ROUTES } from "../../assets/js/config.js"; + +const els = { + resendBtn: document.getElementById("resendBtn"), + formError: document.getElementById("formError"), + formSuccess: document.getElementById("formSuccess"), + emailText: document.getElementById("emailText") +}; + +function getStoredEmail() { + return ( + sessionStorage.getItem("aidloop_pending_verification_email") || + localStorage.getItem("aidloop_organizer_email") || + "" + ); +} + +function setLoading(isLoading) { + els.resendBtn.disabled = isLoading; + els.resendBtn.textContent = isLoading ? "Resending..." : "Resend Email"; +} + +async function resendVerification() { + const email = getStoredEmail(); + + els.formError.textContent = ""; + els.formSuccess.textContent = ""; + + if (!email) { + els.formError.textContent = "No email found. Please sign up again."; + return; + } + + try { + setLoading(true); + + const result = await apiRequest("/auth/resend-otp", { + method: "POST", + body: JSON.stringify({ email }) + }); + + els.formSuccess.textContent = + result.message || "Verification email sent successfully."; + } catch (error) { + els.formError.textContent = error.message || "Failed to resend verification email."; + } finally { + setLoading(false); + } +} + +document.addEventListener("DOMContentLoaded", () => { + const email = getStoredEmail(); + + if (email) { + els.emailText.textContent = `Sent to: ${email}`; + } else { + els.emailText.textContent = ""; + } + + els.resendBtn.addEventListener("click", resendVerification); +}); + + + + + + + + diff --git a/Organizer/volunteers/volunteers.css b/Organizer/volunteers/volunteers.css new file mode 100644 index 0000000..b045df3 --- /dev/null +++ b/Organizer/volunteers/volunteers.css @@ -0,0 +1,406 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: "Poppins", sans-serif; +} + +body { + background: #f4f7fb; + color: #1f2f46; +} + +.dashboard-layout { + display: flex; + min-height: 100vh; +} + +.sidebar { + width: 15rem; + background: #1f3b63; + color: #ffffff; + padding: 1.5rem 1rem; + flex-shrink: 0; +} + +.sidebar-logo { + text-align: center; + margin-bottom: 2rem; +} + +.sidebar-logo img { + width: 5.5rem; + max-width: 100%; +} + +.sidebar-menu { + list-style: none; + display: flex; + flex-direction: column; + gap: 0.55rem; +} + +.sidebar-menu li a, +.sidebar-logout { + width: 100%; + display: flex; + align-items: center; + gap: 0.85rem; + color: #ffffff; + text-decoration: none; + padding: 0.95rem 1rem; + border-radius: 0.65rem; + font-size: 0.96rem; + border: none; + background: transparent; + cursor: pointer; +} + +.sidebar-menu li.active a, +.sidebar-menu li a:hover { + background: #1f80ea; +} + +.sidebar-logout:hover { + background: #dc2626; +} + +.main-content { + flex: 1; + display: flex; + flex-direction: column; +} + +.topbar { + background: #ffffff; + padding: 2rem 2.2rem 1.2rem; + display: flex; + justify-content: space-between; + align-items: flex-start; +} + +.page-title { + font-size: 2rem; + font-weight: 700; + color: #223f6b; +} + +.page-subtitle { + margin-top: 0.3rem; + font-size: 0.9rem; + color: #6b7280; +} + +.topbar-right { + display: flex; + align-items: center; + gap: 0.9rem; +} + +.icon-btn { + width: 2.7rem; + height: 2.7rem; + border: none; + border-radius: 50%; + background: transparent; + color: #223f6b; + font-size: 1.35rem; + cursor: pointer; +} + +.page-content { + padding: 2rem 2.2rem; +} + +.toolbar { + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; + flex-wrap: wrap; + margin-bottom: 2rem; +} + +.filter-tabs { + display: flex; + align-items: center; + gap: 2rem; + flex-wrap: wrap; +} + +.filter-btn { + border: none; + background: transparent; + color: #111827; + font-size: 1rem; + cursor: pointer; + padding-bottom: 0.35rem; +} + +.filter-btn.active { + color: #1f80ea; + font-weight: 600; +} + +.search-box { + width: 100%; + max-width: 17rem; + display: flex; + align-items: center; + border: 0.08rem solid #8fa1ba; + border-radius: 0.55rem; + overflow: hidden; + background: #ffffff; +} + +.search-box input { + flex: 1; + border: none; + outline: none; + padding: 0.9rem 0.9rem; + font-size: 0.9rem; + background: transparent; +} + +.search-box button { + width: 3rem; + border: none; + background: transparent; + color: #1f80ea; + cursor: pointer; + font-size: 1rem; +} + +.stats-grid { + display: grid; + gap: 2rem; + margin-bottom: 2rem; +} + +.three-cards { + grid-template-columns: repeat(3, minmax(0, 1fr)); + max-width: 48rem; +} + +.stat-card { + background: #ffffff; + border: 0.08rem solid #9ca9bb; + border-radius: 0.85rem; + overflow: hidden; +} + +.stat-number { + text-align: center; + font-size: 2.2rem; + font-weight: 700; + color: #111111; + padding: 1rem 0 0.7rem; +} + +.stat-footer { + border-top: 0.08rem solid #c9d2dd; + display: flex; + justify-content: center; + align-items: center; + gap: 0.55rem; + padding: 0.95rem 1rem; + color: #526176; + font-size: 0.86rem; +} + +.stat-footer i { + color: #1f80ea; + font-size: 1rem; +} + +.table-wrapper { + background: #ffffff; + border-radius: 0.85rem; + overflow: hidden; + border: 0.08rem solid #d8dfe8; +} + +table { + width: 100%; + border-collapse: collapse; +} + +thead { + background: #eef2f6; +} + +th, +td { + padding: 1rem; + text-align: left; + font-size: 0.92rem; +} + +th { + color: #35527a; + font-weight: 600; +} + +tbody tr { + border-bottom: 0.08rem solid #d7dde6; +} + +tbody tr:last-child { + border-bottom: none; +} + +.volunteer-name { + display: flex; + align-items: center; + gap: 0.8rem; +} + +.avatar { + width: 2.1rem; + height: 2.1rem; + border-radius: 50%; + object-fit: cover; + flex-shrink: 0; +} + +.badge { + display: inline-block; + min-width: 6.8rem; + text-align: center; + padding: 0.55rem 0.9rem; + border-radius: 0.4rem; + color: #ffffff; + font-size: 0.84rem; + font-weight: 600; +} + +.confirmed { + background: #26a65b; +} + +.attendance-wrap { + display: flex; + align-items: center; +} + +.attendance-box { + width: 1.8rem; + height: 1.8rem; + border: 0.1rem solid #1f80ea; + border-radius: 0.45rem; + display: inline-flex; + align-items: center; + justify-content: center; + background: #ffffff; + cursor: pointer; + transition: 0.2s ease; +} + +.attendance-box.checked { + background: #1f80ea; + color: #ffffff; +} + +.attendance-box:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +.row-actions { + text-align: center; + color: #223f6b; + font-size: 1.1rem; +} + +.row-actions button { + border: none; + background: transparent; + color: inherit; + cursor: pointer; +} + +.table-footer { + margin-top: 1rem; + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; + flex-wrap: wrap; + color: #4b5563; + font-size: 0.9rem; +} + +.pagination { + display: flex; + align-items: center; + gap: 0.35rem; + flex-wrap: wrap; +} + +.page-btn { + border: 0.08rem solid #d6dde6; + background: #ffffff; + color: #1f2f46; + padding: 0.42rem 0.75rem; + border-radius: 0.3rem; + cursor: pointer; + font-size: 0.8rem; +} + +.page-btn.active { + background: #1f80ea; + color: #ffffff; + border-color: #1f80ea; +} + +.qualification-cell { + font-size: 0.84rem; + font-weight: 500; +} + +.qualification-cell.qualified { + color: #15803d; +} + +.qualification-cell.pending { + color: #9ca3af; +} + +@media (max-width: 68rem) { + .three-cards { + grid-template-columns: 1fr; + max-width: 100%; + } +} + +@media (max-width: 64rem) { + .dashboard-layout { + flex-direction: column; + } + + .sidebar { + width: 100%; + } + + .topbar { + flex-direction: column; + gap: 1rem; + } + + .toolbar { + flex-direction: column; + align-items: stretch; + } + + .search-box { + max-width: 100%; + } + + .table-wrapper { + overflow-x: auto; + } + + table { + min-width: 46rem; + } +} \ No newline at end of file diff --git a/Organizer/volunteers/volunteers.html b/Organizer/volunteers/volunteers.html new file mode 100644 index 0000000..49d5605 --- /dev/null +++ b/Organizer/volunteers/volunteers.html @@ -0,0 +1,178 @@ + + + + + + AIDLoop Volunteers + + + + + + + + + +
    + + +
    +
    +
    +

    Volunteers

    +

    Manage Volunteers across all events.

    +
    + +
    + + + +
    +
    + +
    +
    +
    + + + +
    + + +
    + +
    +
    +
    0
    + +
    + +
    +
    0
    + +
    + +
    +
    0
    + +
    +
    + +
    + + + + + + + + + + + + + + + + + +
    NameEmailStatusAttendanceCertificate
    Loading volunteers...
    +
    + + +
    +
    +
    + + + + diff --git a/Organizer/volunteers/volunteers.js b/Organizer/volunteers/volunteers.js new file mode 100644 index 0000000..41963ce --- /dev/null +++ b/Organizer/volunteers/volunteers.js @@ -0,0 +1,202 @@ +import { apiRequest } from "../../assets/js/api.js"; +import { requireRole } from "../../assets/js/auth.js"; +import { logout } from "../../assets/js/logout.js"; +import { ROUTES } from "../../assets/js/config.js"; + +const eventId = new URLSearchParams(window.location.search).get("eventId"); + +const els = { + table: document.getElementById("volunteerTable"), + total: document.getElementById("totalRegistered"), + attended: document.getElementById("attendedCount"), + noShow: document.getElementById("noShowCount"), + searchInput: document.getElementById("searchInput"), + tableCountText: document.getElementById("tableCountText"), + filterBtns: document.querySelectorAll(".filter-btn"), + logoutBtn: document.getElementById("logoutBtn") +}; + +let allVolunteers = []; +let currentFilter = "all"; + +function getStatus(v) { + return String(v.status || "confirmed").toLowerCase(); +} + +function getAttendance(v) { + return String(v.attendance || "").toLowerCase(); +} + +function getDisplayName(v) { + return v.user?.fullName || "Unknown"; +} + +function getEmail(v) { + return v.user?.email || "—"; +} + +function getAvatar(v) { + return v.user?.profileImage || "https://i.pravatar.cc/100?img=12"; +} + +function getQualification(v) { + return getAttendance(v) === "attended" ? "Qualified for certificate" : "Pending attendance"; +} + +function renderStats() { + const total = allVolunteers.length; + const attended = allVolunteers.filter((v) => getAttendance(v) === "attended").length; + const noShow = Math.max(0, total - attended); + + els.total.textContent = total; + els.attended.textContent = attended; + els.noShow.textContent = noShow; +} + +function renderTable() { + const query = els.searchInput.value.trim().toLowerCase(); + + let filtered = [...allVolunteers]; + + if (currentFilter !== "all") { + filtered = filtered.filter((v) => getStatus(v) === currentFilter); + } + + if (query) { + filtered = filtered.filter((v) => { + const searchable = `${getDisplayName(v)} ${getEmail(v)} ${getQualification(v)}`.toLowerCase(); + return searchable.includes(query); + }); + } + + els.tableCountText.textContent = `Showing ${filtered.length} of ${allVolunteers.length} entries`; + + if (!filtered.length) { + els.table.innerHTML = ` + No volunteers found + `; + return; + } + + els.table.innerHTML = filtered.map((v) => { + const id = v._id; + const attended = getAttendance(v) === "attended"; + + return ` + + +
    + ${getDisplayName(v)} + ${getDisplayName(v)} +
    + + ${getEmail(v)} + Confirmed + +
    + +
    + + + ${attended ? "Qualified for certificate" : "Pending attendance"} + + + + + + `; + }).join(""); + + attachAttendanceHandlers(); +} + +function attachAttendanceHandlers() { + document.querySelectorAll(".attendance-box").forEach((btn) => { + btn.addEventListener("click", async () => { + const id = btn.dataset.id; + const targetVolunteer = allVolunteers.find((v) => v._id === id); + const alreadyAttended = getAttendance(targetVolunteer) === "attended"; + + if (alreadyAttended) return; + + try { + btn.disabled = true; + + await apiRequest(`/applications/registrations/${id}/attendance`, { + method: "PATCH", + body: JSON.stringify({ status: "attended" }) + }); + + allVolunteers = allVolunteers.map((v) => + v._id === id + ? { + ...v, + attendance: "attended", + certificateQualified: true + } + : v + ); + + renderStats(); + renderTable(); + } catch (err) { + alert(err.message || "Failed to update attendance"); + btn.disabled = false; + } + }); + }); +} + +async function loadVolunteers() { + const data = await apiRequest(`/applications/events/${eventId}/registrations`); + const volunteers = Array.isArray(data) ? data : data.data || []; + allVolunteers = volunteers; + + renderStats(); + renderTable(); +} + +function bindFilters() { + els.filterBtns.forEach((btn) => { + btn.addEventListener("click", () => { + els.filterBtns.forEach((b) => b.classList.remove("active")); + btn.classList.add("active"); + currentFilter = btn.dataset.filter; + renderTable(); + }); + }); +} + +els.searchInput.addEventListener("input", renderTable); + +els.logoutBtn.addEventListener("click", () => { + logout(ROUTES.organizerLogin); +}); + +document.addEventListener("DOMContentLoaded", async () => { + await requireRole("organizer", ROUTES.organizerLogin); + + if (!eventId) { + alert("No event selected"); + window.location.href = ROUTES.eventListing; + return; + } + + bindFilters(); + + try { + await loadVolunteers(); + } catch (err) { + els.table.innerHTML = ` + Failed to load volunteers + `; + } +}); diff --git a/README.md b/README.md deleted file mode 100644 index f57ee73..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# aidloop-web -Web app for AidLoop platform diff --git a/Images/AIDLoopLogo.jpeg b/assets/Images/AIDLoopLogo.jpeg similarity index 100% rename from Images/AIDLoopLogo.jpeg rename to assets/Images/AIDLoopLogo.jpeg diff --git a/Images/LandingPageImage.jpeg b/assets/Images/LandingPageImage.jpeg similarity index 100% rename from Images/LandingPageImage.jpeg rename to assets/Images/LandingPageImage.jpeg diff --git a/assets/Images/Logo.png b/assets/Images/Logo.png new file mode 100644 index 0000000..24513be Binary files /dev/null and b/assets/Images/Logo.png differ diff --git a/Images/Sign_Up.png b/assets/Images/Sign_Up.png similarity index 100% rename from Images/Sign_Up.png rename to assets/Images/Sign_Up.png diff --git a/Images/certificate.png b/assets/Images/certificate.png similarity index 100% rename from Images/certificate.png rename to assets/Images/certificate.png diff --git a/Images/check_circle.png b/assets/Images/check_circle.png similarity index 100% rename from Images/check_circle.png rename to assets/Images/check_circle.png diff --git a/Images/clipboard.png b/assets/Images/clipboard.png similarity index 100% rename from Images/clipboard.png rename to assets/Images/clipboard.png diff --git a/Images/event.png b/assets/Images/event.png similarity index 100% rename from Images/event.png rename to assets/Images/event.png diff --git a/Images/instagram.png b/assets/Images/instagram.png similarity index 100% rename from Images/instagram.png rename to assets/Images/instagram.png diff --git a/Images/linkedin.png b/assets/Images/linkedin.png similarity index 100% rename from Images/linkedin.png rename to assets/Images/linkedin.png diff --git a/Images/shield_check.png b/assets/Images/shield_check.png similarity index 100% rename from Images/shield_check.png rename to assets/Images/shield_check.png diff --git a/Images/user.png b/assets/Images/user.png similarity index 100% rename from Images/user.png rename to assets/Images/user.png diff --git a/assets/Images/volunteer.png b/assets/Images/volunteer.png new file mode 100644 index 0000000..eefafd0 Binary files /dev/null and b/assets/Images/volunteer.png differ diff --git a/Images/x.png b/assets/Images/x.png similarity index 100% rename from Images/x.png rename to assets/Images/x.png diff --git a/assets/css/badges.css b/assets/css/badges.css new file mode 100644 index 0000000..0c78263 --- /dev/null +++ b/assets/css/badges.css @@ -0,0 +1,30 @@ +.status-badge, .badge { + display: inline-block; + min-width: 6.8rem; + text-align: center; + padding: 0.58rem 0.95rem; + border-radius: 0.42rem; + color: #fff; + font-size: 0.84rem; + font-weight: 600; +} + +.status-issued, .confirmed { + background: #2e8b57; +} + +.status-pending { + background: #f5ae42; +} + +.status-cancelled { + background: #dc2626; +} + +.status-draft { + background: #6f809b; +} + +.status-completed { + background: #1f80ea; +} diff --git a/assets/css/forms.css b/assets/css/forms.css new file mode 100644 index 0000000..3b352f2 --- /dev/null +++ b/assets/css/forms.css @@ -0,0 +1,42 @@ +.form-group { + margin-bottom: 1rem; +} + +.form-group label { + display: block; + font-size: 0.9rem; + font-weight: 500; + margin-bottom: 0.35rem; +} + +input, textarea, select { + width: 100%; + padding: 0.85rem 0.95rem; + border: 1px solid #d1d5db; + border-radius: 0.7rem; + background: #fff; + outline: none; +} + +textarea { + min-height: 6rem; + resize: vertical; +} + +.primary-btn { + border: none; + background: #223f6b; + color: #fff; + padding: 0.95rem 1.35rem; + border-radius: 0.75rem; + cursor: pointer; +} + +.secondary-btn { + border: 1px solid #223f6b; + background: transparent; + color: #223f6b; + padding: 0.95rem 1.35rem; + border-radius: 0.75rem; + cursor: pointer; +} diff --git a/assets/css/layout.css b/assets/css/layout.css new file mode 100644 index 0000000..fa7d4ff --- /dev/null +++ b/assets/css/layout.css @@ -0,0 +1,92 @@ +.dashboard-layout { + display: flex; + min-height: 100vh; +} + +.sidebar { + width: 15rem; + background: #1f3b63; + color: #fff; + padding: 1.5rem 1rem; + flex-shrink: 0; +} + +.sidebar-logo { + text-align: center; + margin-bottom: 2rem; +} + +.sidebar-logo img { + width: 5.5rem; +} + +.sidebar-menu { + list-style: none; + display: flex; + flex-direction: column; + gap: 0.55rem; +} + +.sidebar-menu li a, .sidebar-logout { + width: 100%; + display: flex; + align-items: center; + gap: 0.85rem; + color: #fff; + padding: 0.95rem 1rem; + border-radius: 0.65rem; + background: transparent; + border: none; + cursor: pointer; +} + +.sidebar-menu li.active a, .sidebar-menu li a:hover { + background: #1f80ea; +} + +.sidebar-logout:hover { + background: #dc2626; +} + +.main-content { + flex: 1; + display: flex; + flex-direction: column; +} + +.topbar { + background: #fff; + padding: 2rem 2.2rem 1.2rem; + display: flex; + justify-content: space-between; + align-items: flex-start; +} + +.page-content { + padding: 2.2rem; +} + +.page-title { + font-size: 2rem; + font-weight: 700; + color: #223f6b; +} + +.page-subtitle { + margin-top: 0.3rem; + font-size: 0.9rem; + color: #6b7280; +} + +@media (max-width: 64rem) { + .dashboard-layout { + flex-direction: column; +} + .sidebar { + width: 100%; +} + .topbar { + flex-direction: column; + gap: 1rem; +} +} diff --git a/assets/css/shared.css b/assets/css/shared.css new file mode 100644 index 0000000..bbe4c62 --- /dev/null +++ b/assets/css/shared.css @@ -0,0 +1,28 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: "Poppins", sans-serif; +} +body { + background: #f4f7fb; + color: #1f2f46; +} +a { + text-decoration: none; +} +button:disabled { + opacity: 0.7; + cursor: not-allowed; +} +.form-error, .success-message { + display: block; + font-size: 0.85rem; + margin-top: 0.5rem; +} +.form-error { + color: #dc2626; +} +.success-message { + color: #16a34a; +} diff --git a/assets/css/tables.css b/assets/css/tables.css new file mode 100644 index 0000000..5acf63d --- /dev/null +++ b/assets/css/tables.css @@ -0,0 +1,34 @@ +.table-wrapper { + background: #fff; + border-radius: 0.9rem; + overflow: hidden; + border: 1px solid #d8dfe8; +} + +table { + width: 100%; + border-collapse: collapse; +} + +thead { + background: #eef2f6; +} + +th, td { + padding: 1rem; + text-align: left; + font-size: 0.92rem; +} + +th { + color: #35527a; + font-weight: 600; +} + +tbody tr { + border-bottom: 1px solid #d7dde6; +} + +tbody tr:last-child { + border-bottom: none; +} diff --git a/assets/js/admin.js b/assets/js/admin.js new file mode 100644 index 0000000..96f040a --- /dev/null +++ b/assets/js/admin.js @@ -0,0 +1,34 @@ +import { getCurrentUser } from "./auth.js"; + +export async function hydrateAdminHeader({ + nameSelector = "#adminName", + roleSelector = "#adminRole", + avatarSelector = "#adminAvatar" +} = {}) { + const nameEl = document.querySelector(nameSelector); + const roleEl = document.querySelector(roleSelector); + const avatarEl = document.querySelector(avatarSelector); + + try { + const profile = await getCurrentUser(); + + if (nameEl) { + nameEl.textContent = profile.fullName || profile.name || "Admin"; + } + + if (roleEl) { + roleEl.textContent = profile.role + ? profile.role.charAt(0).toUpperCase() + profile.role.slice(1) + : "Admin"; + } + + if (avatarEl && profile.profileImage) { + avatarEl.src = profile.profileImage; + } + + return profile; + } catch (error) { + console.error("Failed to load admin header:", error.message); + return null; + } +} \ No newline at end of file diff --git a/assets/js/api.js b/assets/js/api.js new file mode 100644 index 0000000..22ba696 --- /dev/null +++ b/assets/js/api.js @@ -0,0 +1,34 @@ +import { API_BASE_URL } from "./config.js"; + +export async function apiRequest(endpoint, options = {}) { + const response = await fetch(`${API_BASE_URL}${endpoint}`, { + credentials: "include", + headers: { + "Content-Type": "application/json", + ...(options.headers || {}) + }, + ...options + }); + + const contentType = response.headers.get("content-type") || ""; + const isJson = contentType.includes("application/json"); + const data = isJson ? await response.json() : await response.blob(); + + if (!response.ok) { + if (isJson) { + throw new Error(data.message || data.error || "Request failed"); + } + throw new Error("Request failed"); + } + + return data; +} + +export function normalizeArray(payload, keys = []) { + if (Array.isArray(payload)) return payload; + for (const key of keys) { + if (Array.isArray(payload?.[key])) return payload[key]; + } + if (Array.isArray(payload?.data)) return payload.data; + return []; +} diff --git a/assets/js/auth.js b/assets/js/auth.js new file mode 100644 index 0000000..f6a36b7 --- /dev/null +++ b/assets/js/auth.js @@ -0,0 +1,27 @@ +import { apiRequest } from "./api.js"; +import { ROUTES } from "./config.js"; + +export async function getCurrentUser() { + try { + return await apiRequest("/users/me"); + } catch { + return await apiRequest("/user/me"); + } +} + +export async function requireRole(role, redirectTo) { + try { + const user = await getCurrentUser(); + if (String(user.role || "").toLowerCase() !== String(role).toLowerCase()) { + window.location.href = redirectTo; + return null; + } + return user; + } catch { + window.location.href = redirectTo; + return null; + } +} + +export const requireAdmin = () => requireRole("admin", ROUTES.adminLogin); +export const requireOrganizer = () => requireRole("organizer", ROUTES.organizerLogin); diff --git a/assets/js/config.js b/assets/js/config.js new file mode 100644 index 0000000..4af86c3 --- /dev/null +++ b/assets/js/config.js @@ -0,0 +1,28 @@ +export const API_BASE_URL = "https://aidloop-backend.onrender.com/api"; + +export const ROUTES = { + home: "/index.html", + + adminLogin: "/Admin/login/admin-login.html", + adminDashboard: "/Admin/dashboard/admin-dashboard.html", + adminProfile: "/Admin/profile/admin-profile.html", + adminVerificationQueue: "/Admin/verification/verification-queue.html", + adminOrganizations: "/Admin/organizations/organization-directory.html", + adminEvents: "/Admin/events/events-oversight.html", + adminFlags: "/Admin/flags/flags.html", + adminUsers: "/Admin/users/user-management.html", + adminCertificates: "/Admin/certificates/certificates.html", + + organizerLogin: "/Organizer/login/organizer-login.html", + organizerSignup: "/Organizer/signup/organizer-signup.html", + organizerVerifyEmail: "/Organizer/verify-email/verify-email.html", + organizerEmailVerified: "/Organizer/email-verified/email-verified.html", + organizerDashboard: "/Organizer/dashboard/organizer-dashboard.html", + organizerCreateEvent: "/Organizer/events/create-event.html", + organizerEventListing: "/Organizer/events/event-listing.html", + organizerEventDetails: "/Organizer/events/event-details.html", + organizerCancelEvent: "/Organizer/events/cancel-event.html", + organizerVolunteers: "/Organizer/volunteers/volunteers.html", + organizerCertificates: "/Organizer/certificates/organizer-certificates.html", + organizerProfile: "/Organizer/profile/organizer-profile.html" +}; diff --git a/assets/js/logout.js b/assets/js/logout.js new file mode 100644 index 0000000..03df49e --- /dev/null +++ b/assets/js/logout.js @@ -0,0 +1,16 @@ +import { apiRequest } from "./api.js"; +import { ROUTES } from "./config.js"; + +export async function logout(redirectTo = ROUTES.home) { + try { + await apiRequest("/auth/logout", { method: "POST" }); + } catch { + // ignore + } finally { + localStorage.removeItem("aidloop_admin_email"); + localStorage.removeItem("aidloop_organizer_email"); + localStorage.removeItem("aidloop_volunteer_email"); + sessionStorage.removeItem("aidloop_pending_verification_email"); + window.location.href = redirectTo; + } +} diff --git a/assets/js/render.js b/assets/js/render.js new file mode 100644 index 0000000..191f306 --- /dev/null +++ b/assets/js/render.js @@ -0,0 +1,19 @@ +export function renderMessageRow(tbody, colSpan, message) { + tbody.innerHTML = ` + + ${message} + + `; +} + +export function toggleEmptyState(tableBody, emptyState, hasData) { + if (!tableBody || !emptyState) return; + + if (hasData) { + tableBody.style.display = ""; + emptyState.style.display = "none"; + } else { + tableBody.style.display = ""; + emptyState.style.display = "flex"; + } +} \ No newline at end of file diff --git a/assets/js/ui.js b/assets/js/ui.js new file mode 100644 index 0000000..45c96c4 --- /dev/null +++ b/assets/js/ui.js @@ -0,0 +1,13 @@ +export function bindFilterButtons(buttons, onChange) { + buttons.forEach((button) => { + button.addEventListener("click", () => { + buttons.forEach((b) => b.classList.remove("active")); + button.classList.add("active"); + onChange(button.dataset.filter); + }); + }); +} + +export function renderMessageRow(tbody, colSpan, message) { + tbody.innerHTML = `${message}`; +} diff --git a/assets/js/utils.js b/assets/js/utils.js new file mode 100644 index 0000000..3d73540 --- /dev/null +++ b/assets/js/utils.js @@ -0,0 +1,30 @@ +export function getQueryParam(name) { + return new URLSearchParams(window.location.search).get(name); +} + +export function formatDate(dateValue, month = "short") { + if (!dateValue) return "—"; + const date = new Date(dateValue); + if (Number.isNaN(date.getTime())) return dateValue; + + return date.toLocaleDateString("en-GB", { + day: "2-digit", + month, + year: "numeric" + }); +} + +export function getLocationText(entity) { + if (typeof entity?.location === "string" && entity.location.trim()) { + return entity.location; + } + + if (entity?.location && typeof entity.location === "object") { + return [ + entity.location.venue, + entity.location.city || entity.location.state + ].filter(Boolean).join(", "); + } + + return entity?.city || entity?.state || "—"; +} diff --git a/index.css b/index.css new file mode 100644 index 0000000..6501eea --- /dev/null +++ b/index.css @@ -0,0 +1,507 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: "Poppins", sans-serif; +} + +html { + scroll-behavior: smooth; +} + +body { + background: #f5f7fa; + color: #223f6b; +} + +a { + text-decoration: none; +} + +.site-header { + width: 100%; + background: #f5f7fa; + position: sticky; + top: 0; + z-index: 50; +} + +.navbar { + max-width: 85rem; + margin: 0 auto; + padding: 1.4rem 2rem; + display: flex; + align-items: center; + justify-content: space-between; + gap: 1.5rem; +} + +.logo img { + width: 11rem; + display: block; +} + +.nav-links { + list-style: none; + display: flex; + align-items: center; + gap: 2rem; +} + +.nav-links a { + color: #223f6b; + font-size: 1.2rem; + font-weight: 700; +} + +.nav-links a:hover { + color: #1f80ea; + transform: translateY(-3px); +} + +.nav-dropdown { + position: relative; +} + +.nav-link-btn { + border: none; + background: transparent; + color: #223f6b; + font-size: 1.05rem; + font-weight: 700; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 0.45rem; +} + +.nav-link-btn:hover { + color: #1f80ea; + transform: translateY(-3px); +} + +.nav-link-btn i { + font-size: 0.8rem; +} + +.dropdown-menu { + position: absolute; + top: calc(100% + 0.8rem); + left: 0; + min-width: 13rem; + background: #ffffff; + border-radius: 0.85rem; + box-shadow: 0 0.8rem 2rem rgba(0, 0, 0, 0.1); + padding: 0.6rem; + display: none; + flex-direction: column; + gap: 0.25rem; + z-index: 100; +} + +.dropdown-menu.show { + display: flex; +} + +.dropdown-menu a { + padding: 0.85rem 1rem; + border-radius: 0.65rem; + color: #223f6b; + font-size: 0.98rem; + font-weight: 700; +} + +.dropdown-menu a:hover { + /* background: #eef5ff; */ + color: #1f80ea; +} + +.nav-buttons { + display: flex; + align-items: center; +} + +.primary-btn, +.secondary-btn { + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 1rem; + font-weight: 600; + transition: 0.2s ease; + cursor: pointer; +} + +.primary-btn { + background: #223f6b; + color: #ffffff; + border: none; +} + +.primary-btn:hover { + background-color: #1f80ea; + transform: translateY(-3px); +} + +.secondary-btn { + background: transparent; + color: #223f6b; + border: 0.12rem solid #1f80ea; +} + +.secondary-btn:hover { + background: #1f80ea; + transform: translateY(-3px); +} + +.nav-signup-btn { + min-width: 10rem; + padding: 0.7rem 1.7rem; + font-size: 1.05rem; +} + +.hero { + max-width: 85rem; + margin: 0 auto; + padding: 3.5rem 2rem 5rem; + display: grid; + grid-template-columns: 1.05fr 0.95fr; + gap: 3rem; + align-items: center; +} + +.hero-text .title { + font-size: 4rem; + line-height: 1; + font-weight: 700; + color: #1f3a5f; +} + +.hero-text .title-large { + font-size: 4rem; + line-height: 1; + font-weight: 700; + margin: 0.4rem 0; +} + +.title .title-large { + margin-top: -80px; +} + +.orange { + color: #ffb347; +} + +.green { + color: #2e8b57; +} + +.description { + max-width: 37rem; + font-size: 1.2rem; + line-height: 1.7; + color: #6c7c95; + margin-top: 1.2rem; +} + +.hero-buttons { + display: flex; + gap: 1.4rem; + margin-top: 1rem; +} + +.hero-buttons .primary-btn, +.hero-buttons .secondary-btn { + min-width: 10rem; + padding: 0.5rem 0.5rem; + font-size: 1.2rem; +} + +.hero-image img { + margin-top: -30px; + width: 600px; + object-fit: cover; + display: block; + border-top-left-radius: 50px; + border-bottom-right-radius: 50px; + box-shadow:0 10px 30px rgba(0,0,0,0.1); + +} + +.section-title { + text-align: center; + margin-bottom: 2.5rem; +} + +.section-title h2 { + font-size: 3.5rem; + font-weight: 800; + color: #222; +} + +.how-it-works { + max-width: 85rem; + margin: 0 auto; + padding: 2rem 2rem 5rem; +} + +.how-it-works-card-container { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 2.3rem; +} + +.how-it-works-card { + background: #ffffff; + border: 0.18rem solid #223f6b; + border-radius: 1.3rem; + padding: 2rem 1.6rem; + text-align: center; + min-height: 24rem; +} + +.card-icon1 img { + width: 5rem; + margin-bottom: 1rem; +} + +.how-it-works-card h4 { + font-size: 1.9rem; + color: #223f6b; + margin-bottom: 1rem; +} + +.how-it-works-card p { + font-size: 1.35rem; + line-height: 1.7; + color: #6c7c95; +} + +.benefits { + max-width: 85rem; + margin: 0 auto; + padding: 1rem 2rem 5rem; +} + +.benefits-title { + text-align: left; +} + +.benefits-card-container { + display: flex; + flex-direction: column; + gap: 1.8rem; +} + +.benefits-card { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 2rem; +} + +.benefit-text { + flex: 1; +} + +.benefits-card h5 { + font-size: 2.6rem; + color: #223f6b; + margin-bottom: 0.8rem; +} + +.benefits-card p { + max-width: 50rem; + font-size: 1.55rem; + line-height: 1.6; + color: #6c7c95; +} + +.card-icon img { + width: 5.4rem; + flex-shrink: 0; +} + +.line { + margin-top: 1.8rem; + border: none; + height: 0.12rem; + background: #1f80ea; +} + +.start-event { + max-width: 85rem; + margin: 0 auto; + padding: 2rem 2rem 5rem; +} + +.start-event-content { + text-align: center; +} + +.start-event-title h3 { + font-size: 3.4rem; + color: #111; + margin-bottom: 2rem; + font-weight: 800; +} + +.start-event-content p { + max-width: 58rem; + margin: 0 auto 2rem; + font-size: 1.9rem; + line-height: 1.6; + color: #222; +} + +.cta-btn { + min-width: 10rem; + padding: 0.7rem 1.7rem; + font-size: 1.2rem; +} + +.footer { + background: #223f6b; + color: #ffffff; + margin-top: 2rem; +} + +.footer-container { + max-width: 85rem; + margin: 0 auto; + padding: 3rem 2rem; + display: grid; + grid-template-columns: 1.5fr 1fr 1fr 1fr; + gap: 2rem; +} + +.footer-logo-text { + font-size: 2.2rem; + margin-bottom: 0.7rem; +} + +.tagline, +.help-text, +.copyright { + color: #dbe4ef; +} + +.footer-col h3 { + font-size: 1.5rem; + margin-bottom: 1rem; +} + +.footer-col ul { + list-style: none; + display: flex; + flex-direction: column; + gap: 0.7rem; +} + +.footer-col a { + color: #dbe4ef; + font-size: 1.2rem; +} + +.footer-col a:hover { + color: #ffffff; +} + +.social-icons { + display: flex; + gap: 0.8rem; + margin-bottom: 1rem; +} + +.social-icons img { + width: 2rem; + height: 2rem; + object-fit: contain; +} + +.copyright { + margin-top: 3rem; + font-size: 0.95rem; +} + +@media (max-width: 68rem) { + .hero { + grid-template-columns: 1fr; + } + + .how-it-works-card-container { + grid-template-columns: 1fr; + } + + .footer-container { + grid-template-columns: 1fr 1fr; + } + + .hero-text .title, + .hero-text .title-large { + font-size: 3rem; + } + + .description, + .benefits-card p, + .start-event-content p { + font-size: 1.2rem; + } + + .benefits-card h5 { + font-size: 2rem; + } +} + +@media (max-width: 48rem) { + .navbar { + flex-wrap: wrap; + justify-content: center; + } + + .nav-links { + flex-wrap: wrap; + justify-content: center; + } + + .hero, + .how-it-works, + .benefits, + .start-event { + padding-left: 1.2rem; + padding-right: 1.2rem; + } + + .footer-container { + grid-template-columns: 1fr; + } + + .hero-buttons { + flex-direction: column; + } + + .hero-buttons .primary-btn, + .hero-buttons .secondary-btn, + .cta-btn { + width: 100%; + } + + .section-title h2 { + font-size: 2.5rem; + } + + .start-event-title h3 { + font-size: 2.3rem; + } +} + + + + + + + + + + + diff --git a/index.html b/index.html index bd25046..6475174 100644 --- a/index.html +++ b/index.html @@ -1,44 +1,317 @@ - - - Aid Loop Organizational login - + + + AIDLoop - Recruit Verified Volunteers + + + + + + -
    -
    -
    -

    Organizer Login

    -

    Sign in to access your organizers dashboard, manage
    volunteer events, and track participant activity

    -
    - + + +
    +
    +
    +

    Recruit Verified

    + +

    + Volun-teers +

    - -
    -
    - - +

    for Your Events

    + +

    + Create and manage volunteer events, connect with verified volunteers, + track attendance, and reward efforts with certificates — all in one + platform to maximize your community impact. +

    + + +
    + +
    + Volunteer event preview +
    +
    -
    - - - +
    +
    +

    How it Works

    +
    + +
    +
    + + Sign up + +

    Sign Up

    +

    + Sign up your organization to start hosting volunteer events. Submit + your details and get verified. +

    - -
    -
    - +
    + + Create event + +

    Create Volunteer Event

    +

    + Create volunteer opportunities using a simple event template. Add + the event details, roles, and number of volunteers needed. +

    +
    -

    Don't have an account?

    Register Organization
    -
    +
    + + Manage volunteers + +

    Manage Volunteers

    +

    + Track registrations, mark attendance, rate volunteers, and issue + certificates from your organizer dashboard. +

    +
    +
    +
    + +
    +
    +

    Benefits

    +
    + +
    +
    +
    +
    Verified Volunteers
    +

    + Connect with volunteers who are registered and tracked through the platform. +

    +
    +
    + + Verified volunteers + +
    + +
    +
    +
    Easy Event Creation
    +

    + Create events quickly using a structured template designed for volunteer activities. +

    +
    +
    + + Easy event creation + +
    + +
    +
    +
    Attendance Tracking
    +

    + Monitor volunteer participation and keep records for each event. +

    +
    +
    + + Attendance tracking + +
    + +
    +
    +
    Volunteer Certificates
    +

    + Recognize volunteers by issuing certificates after successful event participation. +

    +
    + + Volunteer certificates + +
    +
    +
    + +
    +
    +
    +

    Start Organizing Volunteer Events Today

    +
    + +

    + Join organizations already using AidLoop to manage volunteer events + and create meaningful community impact. +

    + + + Sign Up + +
    +
    +
    + +
    + +
    + + + + + + + + + + + + + + diff --git a/index.js b/index.js new file mode 100644 index 0000000..d24b7fb --- /dev/null +++ b/index.js @@ -0,0 +1,91 @@ +import { apiRequest } from "./assets/js/api.js"; +import { ROUTES } from "./assets/js/config.js"; + +const els = { + loginMenuBtn: document.getElementById("loginMenuBtn"), + loginDropdown: document.getElementById("loginDropdown"), + organizerLoginLink: document.getElementById("organizerLoginLink"), + adminLoginLink: document.getElementById("adminLoginLink"), + signupBtnTop: document.getElementById("signupBtnTop"), + signupBtnHero: document.getElementById("signupBtnHero"), + signupBtnBottom: document.getElementById("signupBtnBottom") +}; + +function updateCTAForOrganizer() { + els.loginMenuBtn.innerHTML = `Dashboard`; + els.loginMenuBtn.dataset.mode = "direct"; + els.loginMenuBtn.onclick = () => { + window.location.href = ROUTES.organizerDashboard; + }; + + els.signupBtnTop.textContent = "Dashboard"; + els.signupBtnTop.href = ROUTES.organizerDashboard; + + els.signupBtnHero.textContent = "Go to Dashboard"; + els.signupBtnHero.href = ROUTES.organizerDashboard; + + els.signupBtnBottom.textContent = "Go to Dashboard"; + els.signupBtnBottom.href = ROUTES.organizerDashboard; +} + +function updateCTAForAdmin() { + els.loginMenuBtn.innerHTML = `Admin Dashboard`; + els.loginMenuBtn.dataset.mode = "direct"; + els.loginMenuBtn.onclick = () => { + window.location.href = ROUTES.dashboard; + }; + + els.signupBtnTop.textContent = "Admin Dashboard"; + els.signupBtnTop.href = ROUTES.dashboard; + + els.signupBtnHero.textContent = "Go to Admin Dashboard"; + els.signupBtnHero.href = ROUTES.dashboard; + + els.signupBtnBottom.textContent = "Go to Admin Dashboard"; + els.signupBtnBottom.href = ROUTES.dashboard; +} + +function bindDropdown() { + els.loginMenuBtn?.addEventListener("click", (event) => { + if (els.loginMenuBtn.dataset.mode === "direct") return; + + event.stopPropagation(); + els.loginDropdown.classList.toggle("show"); + }); + + document.addEventListener("click", () => { + els.loginDropdown.classList.remove("show"); + }); + + els.loginDropdown?.addEventListener("click", (event) => { + event.stopPropagation(); + }); +} + +async function detectExistingSession() { + try { + let user; + + try { + user = await apiRequest("/user/me"); + } catch { + user = await apiRequest("/users/me"); + } + + const role = String(user.role || "").toLowerCase(); + + if (role === "organizer") { + updateCTAForOrganizer(); + } else if (role === "admin") { + updateCTAForAdmin(); + } + } catch { + // guest user + } +} + +document.addEventListener("DOMContentLoaded", async () => { + bindDropdown(); + await detectExistingSession(); +}); + diff --git a/logo aidloop.png b/logo aidloop.png deleted file mode 100644 index 649fb66..0000000 Binary files a/logo aidloop.png and /dev/null differ diff --git a/script.js b/script.js deleted file mode 100644 index e69de29..0000000 diff --git a/signUp.html b/signUp.html deleted file mode 100644 index e69de29..0000000 diff --git a/style.css b/style.css deleted file mode 100644 index d41279a..0000000 --- a/style.css +++ /dev/null @@ -1,574 +0,0 @@ - -*{ - margin:0; - padding:0; - box-sizing:border-box; -} - -header{ - margin-bottom: 60px; -} - -body{ - background:#f4f6f9; - padding:40px 60px; - color:#243a5e; - font-family: "Poppins", sans-serif; -} - - - -.navbar{ - display:flex; - justify-content:space-between; - align-items:center; -} - -.logo{ - display:flex; - align-items:center; - gap:10px; - font-size:50px; - font-weight:700; -} - - -.logo img{ - width:150px; -} - - -.nav-links{ - display:flex; - gap:30px; - list-style:none; -} - -.nav-links a{ - text-decoration:none; - color:#243a5e; - font-weight:600; -} - -.about:hover, .contact:hover, .current:hover, .login:hover { - background-color: powderblue; - transform: translateY(-3px); -} - -/* .nav-links:hover { - background-color: #e008aa; - transform: translateY(-3px); -} */ - -.nav-buttons{ - display:flex; - gap:15px; -} - -/* .login{ - background:none; - border:none; - font-weight:600; - cursor:pointer; -} */ -/* -.register{ - background:#243a5e; - color:white; - border:none; - padding:10px 20px; - border-radius:10px; - cursor:pointer; -} */ - - - -.hero{ - display:flex; - align-items:center; - justify-content:space-between; - gap:60px; -} - -.hero-text{ - margin-top: -11px; - max-width:650px; -} - -.title{ - - font-size:50px; - font-weight:700; -} - -.title-large{ - - font-size:50px; - font-weight:700; - margin:-12px 0; -} - -.orange{ - color:#FFB347; -} - -.green{ - color:#1FDD19; -} - -.description{ - font-size:18px; - color:#6b7c93; - line-height:1.6; -} - -.hero-buttons{ - margin-top:20px; - display:flex; - gap:50px; -} - -.primary-btn{ - background:#243a5e; - color:white; - border:none; - padding:10px 20px; - border-radius:15px; - cursor:pointer; -} - -.primary-btn:hover { - background-color: #e6c200; - transform: translateY(-3px); -} - -/* .secondary-btn{ - background:#243a5e; - color:#f4a742; - border:none; - padding:16px 28px; - border-radius:16px; - cursor:pointer; -} */ - -.hero-image img{ - width:550px; - border-top-left-radius: 50px; - border-bottom-right-radius: 50px; - /* border-radius:20px; */ - box-shadow:0 10px 30px rgba(0,0,0,0.1); -} - - -.how-it-works { - padding: 5rem 2rem; - /* background: #f9f9f9; - color: #333; */ -} - -.how-it-works-title { - text-align: center; -} - -.how-it-works-title h2 { - font-size: 3.2rem; - font-weight: 500; -} - -/* .how-it-works-card-container { - display: flex; - gap: 2rem; - margin-top: 1rem; - -} */ - -.how-it-works-card-container { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 30px; - /* max-width: 1000px; */ - margin: 0 auto; -} - - -/* @media (max-width: 640px) { - .how-it-works-card-container { - flex-direction: column; - } -} */ - - - -.how-it-works-card { - background: #fff; - border-radius: 10px; - padding: 20px; - width:280px; - box-shadow: 0 4px 10px rgba(0,0,0,0.1); - transition: 0.3s ease; - align-items: center; - display: flex; - flex-direction: column; - gap: 1rem; - border: #1f3a5f solid 3px; - -} - -.how-it-works-card:hover { - transform: translateY(-5px); -} - -.how-it-works-card h4 { - font-weight: 600; - font-size: 1.2rem; -} - -.how-it-works-card p { - color: #555; - line-height: 1.5; -} - - - -.card-icon1 { - display: flex; - justify-content: center; - align-items: center; -} - - - - -/* .Benefits { - /* padding: 50px 20px; - -/* } */ - - -.Benefits-title h2 { - font-size: 4.2rem; - /* font-weight: 500; */ - -} - -.Benefits-card-container { - display: grid; - gap:30px; - - -} - -/* .Benefits-card-container { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 30px; - max-width: 1000px; - margin: 0 auto; -} */ - -/* @media (max-width: 1000px) { - .Benefits-card-container { - flex-direction: column; - } -} */ - -.Benefits-card { - justify-content: space-between; - /* border: 3px; - border-color: #1FDD19; */ - /* padding: 20px; */ -} - -.Benefits-card h5 { - margin-top: -110px; - text-align: left; - /* max-width: 75%; */ - /* font-weight: 400; */ - font-size: 2.5rem; - color: #1f3a5f; -} - -.Benefits-card p { - text-align: left; - /* max-width: 75%; */ - /* padding: 10px; */ - /* margin-left: -20px; */ - color: #1f3a5f; - -} - -.card-icon { - display: flex; - - background-color: #fff; - border: #ffb347 solid 2px; - width: 11rem; - height: 11rem; - justify-content: center; - align-items: center; - border-radius: 90px; - color: #057DF2; - margin-left:55rem; - /* float: right; */ - - -} - -.line{ - border:3px; - height: 2px; - background-color: #057df2; - margin: 30px 0; - width: 750px; -} - -.start-event{ - margin-top: 50px; - display: flex; - justify-content: center; - align-items: center; - text-align: center; - min-height: 70vh; - padding: 20px; -} - -.start-event-content{ - max-width: 1000px; - color: #2c4a6f; - -} - -.start-event-title { - color: #2c4a6f; -} - -.start-event-content h3 { - font-size: 2.5rem; - margin-bottom: 20px; -} - -.start-event-content p { - font-size: 1.2rem; - margin-bottom: 30px; - line-height: 1.6; -} - - -.footer{ - margin-top: 10px; - background:#2c4a6f; - color:white; - padding:20px 10%; -} - -.footer-container{ - display:flex; - justify-content:space-between; - flex-wrap:wrap; - gap:30px; -} - -.footer-col{ - flex:1; - min-width:200px; -} - -.logo{ - font-size:20px; - font-weight:bold; - margin-bottom:10px; -} - -.tagline{ - color:#cbd5e1; - margin-bottom: 100px; - font-size: 15px; -} - -.footer-col h3{ - margin-bottom:15px; - font-size:18px; -} - -.footer-col ul{ - list-style:none; -} - -.footer-col ul li{ - margin-bottom:10px; -} - -.footer-col ul li a{ - color:#cbd5e1; - text-decoration:none; - transition:0.3s; -} - -.footer-col ul li a:hover{ - color:white; -} - -.social-icons{ - display:flex; - gap:15px; - margin-top:10px; - margin-bottom:15px; -} - -.social-icons a{ - background:white; - color:#2c4a6f; - width:40px; - height:40px; - display:flex; - align-items:center; - justify-content:center; - border-radius:6px; - font-size:18px; - text-decoration:none; - transition:0.3s; -} - -.social-icons a:hover{ - background:#dbeafe; -} - -.help-text{ - color:#cbd5e1; -} - -.copyright{ - font-size:10px; - color:#cbd5e1; -} -* { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -body { - background-color: #f6f3f2; - font-family: sans-serif; - display: flex; - justify-content: center; - height: 100vh; -} - -.form-container { - background-color: #fff; - text-align: center; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - margin-bottom: 15px; - padding: 30px; - gap: 30px; -} - -.logo-wrapper { - width: 100px; - height: 100px; - display: flex; - justify-content: center; - align-items: center; -} - -.form-title { - text-align: center; - color: black; - display: flex; - flex-direction: column; - margin-bottom: 1px; - gap: 10px; -} - -#sign { - font-size: 9px; - text-align: center; - color: #99A5B5; - -} -.form { - display: flex; - flex-direction: column; - gap: 20px; -} - -.form-group { - display: flex; - flex-direction: column; -} - -.form-group label { - align-self: flex-start; - font-size: 10px; - font-weight: 400; - color: black; - margin-bottom: 10px; -} - -.form-group input { - display: inline-block; - width: 300px; - height: 40px; - border: 2px solid #999; - border-radius: 10px; -} - - - -.btn-container { - display: flex; - flex-direction: column; - align-items: center; - gap: 10px; - -} - -.btn-org { - width: 300px; - height: 40px; - text-align: center; - background-color: #1F3A5F; - border-radius: 15px; - color: white; - font-size: 15px; -} - -.link { - display: flex; - gap: 20px; -} - -.link a { - display: inline-block; - justify-content: space-around; - text-decoration: none; - color: #8190A5; - border-right: 1px solid #8190A5; - font-size: 10px; - margin-bottom: 40px; - padding: 0 20px; -} - -.link a:last-child { - border-right: none; -} - - -.acc { - display: flex; - color: #8190A5; - font-size: 10px; - -} - -.acc a { - text-decoration: none; - color:#1F3A5F ; - font-size: 10px; - -} -