Admin Portal
← Back to website

Events

Click any event to edit — changes go live on the website immediately

Scholars

Manage the scholars shown on the website

Countdown Timer

Set the date shown on the homepage banner

Note: use 24hr time — e.g. 1:00 PM = 13:00, 5:00 PM = 17:00

FAQ

Edit questions and answers shown on the website

Matches

Create matches to enable private messaging between attendees

Seating Arrangement

Suggest which attendees to seat together for conversation at the event, based on questionnaire compatibility — separate from confirmed matches

80–100% Excellent 60–79% Good Below 60% Low Approved

Questionnaire Responses

All submitted matchmaking questionnaires

#NameGenderAgeCityPaidCultural BackgroundTimelineSubmitted

Match Dashboard

Track all approved matches and their progress

#Person 1Person 2ScoreEventStatusNotesActions

Broadcast Message

Send an announcement to all subscribers — shown on their messages page

Sent Broadcasts

Send Email

Sends real emails straight from the website via your Vercel email backend — no Google Sheet involved

Use {{name}} anywhere you want each recipient's own name inserted.

Needs a Vercel Email API URL configured in Settings — this uses the same connection your questionnaire reminders already send through.

Confirmed Attendees

Mark subscribers as confirmed for a specific event

NameEmailGenderCityConfirmed ForActions

Subscribers

Everyone who signed up for event notifications

#NameEmailGenderCityAgeJoined

Event Registrations

Signups per event — written straight to Firestore

Import Legacy Data

Bring in registrations or questionnaire responses from an old Google Sheet (.xlsx or .csv)

If a registration was ever deleted and re-imported, questionnaire responses linked to the old one can end up pointing at nothing — which quietly hides their real paid status. This re-checks every link for an event and fixes any that are broken, without needing to re-upload anything.

Works with a Google Sheets export (File → Download → .xlsx or .csv). If the file has multiple tabs (like "Form Responses 1" / "Form Responses 2"), you'll pick which one below.

Edit Questionnaire

Add, remove, and reorder questions — changes apply going forward

⚠️ Fields marked Protected are read directly by the matching algorithm to calculate compatibility. You can still edit or delete them — this won't stop you — but doing so may affect how well matching works, especially for people who already filled out the questionnaire before the change.

Nothing here affects the live questionnaire page until it's rebuilt to read from this configuration — for now, this is where you shape what it should become.

Settings

Configure integrations and preferences

Firebase Database

Seed Firebase with default events, FAQs and scholars. Run this once to populate your database.

Email Backend (Vercel)

Paste your deployed Vercel function URL below. This is what actually sends every email — shortlisting, campaigns, replies, reminders — straight from your Gmail account.

How to set this up on Vercel
  1. Get a Gmail App Password: Google Account → Security → 2-Step Verification → App Passwords. Generate one, save the 16-character code somewhere safe
  2. In your Vercel project, add the two files below — api/send-email.js and package.json — exactly at those paths (create an api folder for the first one)
  3. In Vercel: Project Settings → Environment Variables, add GMAIL_USER (your Gmail address) and GMAIL_APP_PASSWORD (the code from step 1)
  4. Deploy the project
  5. Copy the live URL, add /api/send-email to the end, and paste it in the field above

Only requests from nikkahpathways.com.au are allowed to trigger a send — if you ever serve the site from another domain too (like a github.io URL for testing), that needs adding to the allowed list inside the code itself.

api/send-email.js
// Vercel Serverless Function — replaces the Apps Script webhook.
// Handles the same request shapes your website already sends:
//   { type: 'custom_email', to, subject, body }        — Send Email + Campaigns
//   { type: 'email_reply', to, subject, subjectHint, body }  — Reply mode
//   { ...registration fields, no type }                 — new registration -> shortlisting email
//
// Required Vercel Environment Variables (Project Settings -> Environment Variables):
//   GMAIL_USER          your Gmail address, e.g. admin@nikkahpathways.com.au
//   GMAIL_APP_PASSWORD  the 16-character App Password from your Google Account
//                        (Google Account -> Security -> 2-Step Verification -> App Passwords)
//
// Only requests from these origins are allowed to trigger a send. Add any
// other domain you actually serve the site from (e.g. a github.io URL you
// test with) to this list.
const ALLOWED_ORIGINS = [
  'https://nikkahpathways.com.au',
  'https://www.nikkahpathways.com.au',
];

const ADMIN_EMAIL = process.env.GMAIL_USER || 'admin@nikkahpathways.com.au';

let nodemailer;
let _transporter;
function getTransporter() {
  if (!_transporter) {
    nodemailer = require('nodemailer');
    _transporter = nodemailer.createTransport({
      service: 'gmail',
      auth: {
        user: process.env.GMAIL_USER,
        pass: process.env.GMAIL_APP_PASSWORD,
      },
    });
  }
  return _transporter;
}

function applyCors(req, res) {
  const origin = req.headers.origin;
  if (ALLOWED_ORIGINS.includes(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin);
  }
  res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
}

module.exports = async (req, res) => {
  applyCors(req, res);

  if (req.method === 'OPTIONS') {
    res.status(200).end();
    return;
  }
  if (req.method !== 'POST') {
    res.status(405).json({ status: 'error', message: 'Use POST' });
    return;
  }

  const data = req.body || {};

  try {
    if (data.type === 'custom_email') {
      await sendCustomEmail(data);
      res.status(200).json({ status: 'ok' });
      return;
    }

    if (data.type === 'email_reply') {
      await sendReplyEmail(data);
      res.status(200).json({ status: 'ok' });
      return;
    }

    if (data.type === 'questionnaire' || data.type === 'questionnaire_reminder_legacy') {
      // Nothing to do — the site already writes questionnaire completion
      // straight to Firestore itself.
      res.status(200).json({ status: 'ok' });
      return;
    }

    if (data.type === 'questionnaire_reminder') {
      await sendQuestionnaireReminderEmail(data);
      res.status(200).json({ status: 'ok' });
      return;
    }

    // Default: a new registration -> send the shortlisting email immediately.
    await sendShortlistingEmail(data);
    res.status(200).json({ status: 'ok' });
  } catch (err) {
    console.error('send-email error:', err);
    res.status(500).json({ status: 'error', message: err.message });
  }
};

async function sendCustomEmail(data) {
  if (!data.to) throw new Error('Missing recipient email');
  const htmlBody = String(data.body || '').replace(/\n/g, '<br>');
  await getTransporter().sendMail({
    from: `"Nikkah Pathways" <${ADMIN_EMAIL}>`,
    to: data.to,
    replyTo: ADMIN_EMAIL,
    subject: data.subject || 'Message from Nikkah Pathways',
    html: htmlBody,
  });
}

async function sendReplyEmail(data) {
  if (!data.to) throw new Error('Missing recipient email');
  // Nodemailer can only send, not search/read Gmail — so unlike the Apps
  // Script version, this can't find and reply inside an existing thread.
  // It sends a fresh email with "Re: " on the subject, which Gmail's own
  // heuristics will usually (not always) still group with the original
  // conversation.
  const subject = data.subjectHint ? `Re: ${data.subjectHint}` : (data.subject || 'Re: Your message');
  const htmlBody = String(data.body || '').replace(/\n/g, '<br>');
  await getTransporter().sendMail({
    from: `"Nikkah Pathways" <${ADMIN_EMAIL}>`,
    to: data.to,
    replyTo: ADMIN_EMAIL,
    subject,
    html: htmlBody,
  });
}

async function sendQuestionnaireReminderEmail(data) {
  if (!data.email) throw new Error('Missing recipient email');
  const name = data.name || '';
  const city = data.city || 'the event';
  const link = data.link || 'https://nikkahpathways.com.au/questionnaire.html';
  const subject = 'Reminder — Please Complete Your Nikkah Pathways Questionnaire';

  const html = `<div style="font-family:Arial,sans-serif;max-width:600px;margin:auto;color:#333;font-size:15px;line-height:1.7">
  <div style="background:#1a1a2e;padding:30px;text-align:center;border-radius:8px 8px 0 0">
    <h1 style="color:#fff;margin:0;font-size:22px">Nikkah Pathways</h1>
    <p style="color:#c9a96e;margin:4px 0 0;font-size:14px">${city} Event</p>
  </div>
  <div style="background:#fff;padding:30px;border:1px solid #e5e5e5">
    <p>Assalamu Alaykum <strong>${name}</strong>,</p>
    <p>Just a friendly reminder to complete your matchmaking questionnaire — it helps us find your best matches inshallah:</p>
    <div style="text-align:center;margin:30px 0">
      <a href="${link}" style="background:#c9a96e;color:#1a1a2e;padding:14px 32px;border-radius:6px;text-decoration:none;font-weight:bold;font-size:16px;display:inline-block">
        Complete the Questionnaire →
      </a>
    </div>
    <p>Questions? Reply to this email or WhatsApp us at <strong>0488 885 770</strong>.</p>
  </div>
  <div style="background:#1a1a2e;padding:20px;text-align:center;border-radius:0 0 8px 8px">
    <p style="color:#fff;margin:0"><strong>The Nikkah Pathways Team</strong></p>
    <a href="mailto:${ADMIN_EMAIL}" style="color:#c9a96e;text-decoration:none">${ADMIN_EMAIL}</a>
  </div>
</div>`;

  await getTransporter().sendMail({
    from: `"Nikkah Pathways" <${ADMIN_EMAIL}>`,
    to: data.email,
    replyTo: ADMIN_EMAIL,
    subject,
    html,
  });
}

async function sendShortlistingEmail(data) {
  const name = (data.fname && data.lname) ? `${data.fname} ${data.lname}`.trim() : (data.name || '').trim();
  const email = (data.email || '').trim();
  const city = (data.city || data.eventCity || data.event_city || '').trim();
  const eventDate = data.date || data.eventDate || 'TBC';
  const timeDisplay = data.eventTime || 'Time to be confirmed';
  const venueDisplay = data.eventVenue || 'Venue to be confirmed';

  if (!email || !city) {
    console.warn('Skipped shortlisting email — missing email or city.', { email, city });
    return;
  }

  const subject = "You've Been Shortlisted for Nikkah Pathways!";

  const html = `<div style="font-family:Arial,sans-serif;max-width:600px;margin:auto;color:#333;font-size:15px;line-height:1.7">
  <div style="background:#1a1a2e;padding:30px;text-align:center;border-radius:8px 8px 0 0">
    <h1 style="color:#fff;margin:0;font-size:22px">Nikkah Pathways</h1>
    <p style="color:#c9a96e;margin:4px 0 0;font-size:14px">${city} Event</p>
    <p style="color:#c9a96e;margin:6px 0 0;font-size:14px">A halal and supportive space for hearts to meet</p>
  </div>
  <div style="background:#fff;padding:30px;border:1px solid #e5e5e5">
    <p>Assalamu Alaykum <strong>${name}</strong>,</p>
    <p>Thank you for registering for the <strong>Nikkah Pathways ${city} Event</strong>.</p>
    <div style="background:#fff8ee;border-left:4px solid #c9a96e;padding:16px 20px;border-radius:4px;margin:20px 0;text-align:center">
      <p style="font-size:20px;margin:0;font-weight:bold;color:#1a1a2e">Congratulations — you've been shortlisted!</p>
    </div>
    <h2 style="color:#1a1a2e;border-bottom:1px solid #e5e5e5;padding-bottom:8px">Event Details</h2>
    <table style="width:100%;font-size:15px">
      <tr><td style="padding:6px 0;width:130px"><strong>City</strong></td><td>${city}</td></tr>
      <tr><td style="padding:6px 0"><strong>Venue</strong></td><td>${venueDisplay}</td></tr>
      <tr><td style="padding:6px 0"><strong>Date</strong></td><td>${eventDate}</td></tr>
      <tr><td style="padding:6px 0"><strong>Time</strong></td><td>${timeDisplay}</td></tr>
      <tr><td style="padding:6px 0"><strong>Cost</strong></td><td>$150 per person</td></tr>
      <tr><td style="padding:6px 0"><strong>Meals</strong></td><td>Included</td></tr>
    </table>
    <p style="background:#f5f5f5;padding:12px 16px;border-radius:4px;font-size:14px">
      Female attendees are encouraged to bring a <strong>mahram</strong>. A <strong>$100 mahram fee</strong> applies.
    </p>
    <h2 style="color:#1a1a2e;border-bottom:1px solid #e5e5e5;padding-bottom:8px">To Secure Your Spot</h2>
    <p>Please make payment within <strong>48 hours</strong>:</p>
    <div style="background:#f9f9f9;border:1px solid #e0e0e0;border-radius:6px;padding:16px 20px">
      <p style="margin:4px 0"><strong>Account Name:</strong> Ilyas Aden</p>
      <p style="margin:4px 0"><strong>BSB:</strong> 182-182</p>
      <p style="margin:4px 0"><strong>Account Number:</strong> 031453087</p>
      <p style="margin:4px 0"><strong>Reference:</strong> <span style="color:#c9a96e;font-weight:bold">NP ${city} - ${name}</span></p>
    </div>
    <h2 style="color:#1a1a2e;border-bottom:1px solid #e5e5e5;padding-bottom:8px">Important Notes</h2>
    <ul style="padding-left:20px;font-size:14px;color:#444;line-height:2">
      <li>Spots cannot be held beyond <strong>3 days</strong> without payment</li>
      <li>Full refund within <strong>1 week</strong> of date confirmation</li>
      <li>This is <strong>not an official invitation</strong> — sent after payment received</li>
    </ul>
    <p>Please <strong>reply with your payment receipt</strong> to confirm your booking.</p>
  </div>
  <div style="background:#1a1a2e;padding:20px;text-align:center;border-radius:0 0 8px 8px">
    <p style="color:#fff;margin:0"><strong>The Nikkah Pathways Team</strong></p>
    <a href="mailto:${ADMIN_EMAIL}" style="color:#c9a96e;text-decoration:none">${ADMIN_EMAIL}</a>
    <p style="color:#888;font-size:12px;margin-top:8px">WhatsApp: 0488 885 770</p>
  </div>
</div>`;

  const text = `Assalamu Alaykum ${name},

Thank you for registering for the Nikkah Pathways ${city} Event.

Congratulations — you've been shortlisted!

────────────────────────────
EVENT DETAILS
────────────────────────────
City:     ${city}
Venue:    ${venueDisplay}
Date:     ${eventDate}
Time:     ${timeDisplay}
Cost:     $150 per person
Meals:    Included

Female attendees are encouraged to bring a mahram.
A $100 mahram fee applies.

────────────────────────────
TO SECURE YOUR SPOT
────────────────────────────
Please pay within 48 hours:

Account Name:   Ilyas Aden
BSB:            182-182
Account Number: 031453087
Reference:      NP ${city} - ${name}

────────────────────────────
IMPORTANT
────────────────────────────
- Spots cannot be held beyond 3 days without payment
- Full refund available within 1 week of date confirmation
- This is not an official invitation — sent after payment received

Please reply with your payment receipt to confirm your booking.

Warm regards,
The Nikkah Pathways Team
${ADMIN_EMAIL}`;

  await getTransporter().sendMail({
    from: `"Nikkah Pathways" <${ADMIN_EMAIL}>`,
    to: email,
    replyTo: ADMIN_EMAIL,
    subject,
    text,
    html,
  });

  console.log('Shortlisting email sent to:', name, `(${email})`);
}
package.json
{
  "name": "nikkah-pathways-email",
  "version": "1.0.0",
  "private": true,
  "dependencies": {
    "nodemailer": "^6.9.14"
  }
}

Feedback

Ideas and messages from visitors

#NameTypeMessageDate