Plan Your Visit landing page for visit.waterschurch.org

Next.js 16 static export + PHP 8 API for cPanel/Apache hosting.

- Location-aware campus finder (IP estimate, optional precise location, ZIP search)
- Three-step Plan Your Visit form with separate email/SMS consent
- PHP API: validation, anti-spam, rate limiting, SQLite lead store,
  Mailchimp and Sinch Engage adapters, idempotent retries via cron
- Attribution capture (utm_*, gclid/gbraid/wbraid, fbclid, ttclid, twclid) and
  consent-aware analytics hooks
- Vitest and PHP test suites, cPanel deployment script and docs

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Waters Church
2026-09-22 13:22:26 -04:00
co-authored by Claude Fable 5.1
commit 78cd0cc550
109 changed files with 16861 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
Require all denied
+40
View File
@@ -0,0 +1,40 @@
<?php
/**
* CLI: export leads as CSV for staff follow-up.
*
* php api/cron/export-leads.php [--since=YYYY-MM-DD] > leads.csv
*
* Contains personal data — store the output securely and delete when done.
*/
declare(strict_types=1);
if (PHP_SAPI !== 'cli') {
http_response_code(404);
exit;
}
require_once dirname(__DIR__) . '/lib/bootstrap.php';
$since = null;
foreach ($argv as $arg) {
if (str_starts_with($arg, '--since=')) {
$since = substr($arg, 8);
}
}
$sql = 'SELECT submission_id, created_at, first_name, last_name, email, phone_e164, campus_id, visit_date, service_time, adults, children, child_age_ranges, questions, email_consent, sms_consent, consent_version, consent_at, variant, attribution, mailchimp_status, sms_status FROM leads';
$params = [];
if ($since) {
$sql .= ' WHERE created_at >= ?';
$params[] = $since;
}
$sql .= ' ORDER BY created_at ASC';
$st = Db::pdo()->prepare($sql);
$st->execute($params);
$out = fopen('php://stdout', 'w');
$first = true;
while ($row = $st->fetch()) {
if ($first) {
fputcsv($out, array_keys($row));
$first = false;
}
fputcsv($out, $row);
}
+29
View File
@@ -0,0 +1,29 @@
<?php
/**
* Cron: retry failed Mailchimp / Sinch deliveries and purge expired leads.
*
* cPanel → Cron Jobs → every 5 minutes:
* VISIT_PRIVATE_DIR=/home/USER/visit-private /usr/local/bin/php /home/USER/visit.waterschurch.org/api/cron/retry-integrations.php >> /home/USER/visit-private/logs/cron.log 2>&1
*
* CLI only — web requests are refused by the .htaccess in this folder AND by the guard below.
*/
declare(strict_types=1);
if (PHP_SAPI !== 'cli') {
http_response_code(404);
exit;
}
require_once dirname(__DIR__) . '/lib/bootstrap.php';
require_once dirname(__DIR__) . '/lib/LeadProcessor.php';
$lock = fopen(Config::dataDir() . '/retry.lock', 'c');
if (!$lock || !flock($lock, LOCK_EX | LOCK_NB)) {
fwrite(STDOUT, gmdate('c') . " retry: another run is in progress\n");
exit(0);
}
$result = LeadProcessor::retryDue(Config::int('RETRY_BATCH_SIZE', 25));
$purged = LeadProcessor::purgeExpired();
fwrite(STDOUT, sprintf("%s retry: checked=%d processed=%d purged=%d mode=%s\n", gmdate('c'), $result['checked'], $result['processed'], $purged, Config::mode()));
flock($lock, LOCK_UN);