join form mirrors scouting73-form (children textarea, interested_in, source w/ schools) + forwards to shared sheet pipeline; firefox joinbtn fix

This commit is contained in:
claude
2026-08-24 13:34:13 -04:00
parent 1f9e214d29
commit f3c483a58e
+54 -12
View File
@@ -1,4 +1,4 @@
import json, os, shutil, datetime, urllib.request import json, os, shutil, datetime, urllib.request, urllib.parse
from pathlib import Path from pathlib import Path
from fastapi import FastAPI, Form from fastapi import FastAPI, Form
from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.responses import HTMLResponse, RedirectResponse
@@ -16,6 +16,9 @@ LEADS = DATA / "leads.jsonl"
EVENTS_LIVE = DATA / "events.json" EVENTS_LIVE = DATA / "events.json"
EVENTS_SEED = Path(__file__).parent / "events.json" EVENTS_SEED = Path(__file__).parent / "events.json"
NTFY_URL = os.environ.get("NTFY_URL", "") NTFY_URL = os.environ.get("NTFY_URL", "")
# Same lead pipeline as the flyer form (scouting73.thewichersfamily.com):
# forward /join posts to that container's /submit (Google Sheet + ntfy push).
FORM_UPSTREAM = os.environ.get("FORM_UPSTREAM", "http://scouting73-form:8000/submit")
app = FastAPI(title="Pack & Troop 73") app = FastAPI(title="Pack & Troop 73")
app.mount("/static", StaticFiles(directory="/app/static"), name="static") app.mount("/static", StaticFiles(directory="/app/static"), name="static")
@@ -90,7 +93,7 @@ nav.links{display:flex;align-items:center;gap:26px;flex-wrap:wrap}
nav.links a{text-decoration:none;font-weight:600;font-size:15px;color:#333D52;padding:6px 0} nav.links a{text-decoration:none;font-weight:600;font-size:15px;color:#333D52;padding:6px 0}
nav.links a:hover{color:#1E2F52} nav.links a:hover{color:#1E2F52}
nav.links a.on{font-weight:700;color:#1E2F52;border-bottom:2px solid #F0AD2D} nav.links a.on{font-weight:700;color:#1E2F52;border-bottom:2px solid #F0AD2D}
a.joinbtn{font-family:'Archivo';font-weight:700;font-size:15px;background:#F0AD2D;color:#1E2F52 !important;padding:11px 22px;border-radius:999px;border-bottom:none !important} a.joinbtn{display:inline-block;line-height:1.2;font-family:'Archivo';font-weight:700;font-size:15px;background:#F0AD2D;color:#1E2F52 !important;padding:11px 22px;border-radius:999px;border-bottom:none !important}
a.joinbtn:hover{background:#DB9714} a.joinbtn:hover{background:#DB9714}
.hero{position:relative;display:flex;align-items:flex-end;min-height:520px} .hero{position:relative;display:flex;align-items:flex-end;min-height:520px}
.hero .wrap{padding-top:110px;padding-bottom:58px;width:100%} .hero .wrap{padding-top:110px;padding-bottom:58px;width:100%}
@@ -499,14 +502,26 @@ def join_body(sent=False):
<div><label>Email <span class="req">*</span></label><input type="email" name="email" required></div> <div><label>Email <span class="req">*</span></label><input type="email" name="email" required></div>
</div> </div>
<div class="frow"> <div class="frow">
<div><label>Phone</label><input name="phone" inputmode="tel"></div> <div><label>Phone</label><input name="phone" inputmode="tel" placeholder="(610) 555-0100"></div>
<div><label>Which program?</label><select name="program"><option value="">Not sure yet</option><option>Cub Scouts (Pack 73, K-5)</option><option>Troop 73 (ages 11-17)</option><option>Volunteering / helping out</option></select></div> <div><label>Interested in <span class="req">*</span></label><select name="interested_in" required><option value="" disabled selected>Select one...</option><option value="Pack 73 (Cub Scouts, K-5)">Pack 73: Cub Scouts (K-5)</option><option value="Troop 73 (Scouts BSA, 11-17)">Troop 73: ages 11-17</option><option value="Both / kids spanning ages">Both: kids spanning ages</option></select></div>
</div> </div>
<div><label>Kids' names and ages</label><input name="children" placeholder="e.g. Maya (7) and Sam (10)"></div> <div><label>Child(ren): names and grades this fall <span class="req">*</span></label><textarea name="children" rows="2" required placeholder="Emma (Kindergarten), Jack (2nd grade)"></textarea><div style="font-size:12.5px;color:#8B93A3;margin-top:4px">List all kids you're considering. Any grade K-12 welcome.</div></div>
<div><label>Questions, or anything you'd like us to know</label><textarea name="message" rows="4"></textarea></div> <div><label>How did you hear about us? <span class="req">*</span></label><select name="source" id="source" required><option value="" disabled selected>Select one...</option>
<optgroup label="Daycares &amp; Preschools"><option>New Hanover Child Care</option><option>Jerusalem Lutheran Nursery</option><option>Goddard School Schwenksville</option><option>St. Teresa Early Learning</option></optgroup>
<optgroup label="Elementary Schools"><option>Schwenksville Elementary</option><option>Marlborough Elementary (Green Lane)</option><option>New Hanover Elementary</option><option>Upper Perkiomen 4/5 Center</option></optgroup>
<optgroup label="Middle &amp; High Schools"><option>Perkiomen Valley Middle School</option><option>Upper Perkiomen Middle School</option><option>Perkiomen Valley High School</option><option>Upper Perkiomen High School</option></optgroup>
<optgroup label="Pack / Troop Events"><option>Back-to-School Night table</option><option>Pack Open House</option><option>Sign-Up Night</option></optgroup>
<optgroup label="Other"><option>Pack 73 / Troop 73 family referral</option><option>BeAScout / searched online</option><option value="Other school or daycare">Other school or daycare</option><option value="Other">Other</option></optgroup>
</select></div>
<div id="source-place" style="display:none"><label>Which school or daycare?</label><input name="source_place" placeholder="e.g. Oak Ridge Elementary"></div>
<div id="source-other" style="display:none"><label>Please tell us how you heard about us</label><input name="source_other" placeholder="e.g. a friend, Facebook, yard sign"></div>
<div><label>Questions, or anything you'd like us to know</label><textarea name="message" rows="3"></textarea></div>
<button class="cta" type="submit" style="justify-self:start;font-size:16px;padding:14px 30px">Send interest form</button> <button class="cta" type="submit" style="justify-self:start;font-size:16px;padding:14px 30px">Send interest form</button>
<div style="font-size:13px;color:#8B93A3">We only use this to reply about Scouting. No newsletters, no sharing.</div> <div style="font-size:13px;color:#8B93A3">We only use this to reply about Scouting. No newsletters, no sharing.</div>
</form></div> </form>
<script>
(function(){var s=document.getElementById('source');if(!s)return;s.addEventListener('change',function(){var v=s.value;document.getElementById('source-place').style.display=v==='Other school or daycare'?'block':'none';document.getElementById('source-other').style.display=v==='Other'?'block':'none';});})();
</script></div>
</div></section> </div></section>
<section style="padding:64px 0 84px"><div class="wrap"> <section style="padding:64px 0 84px"><div class="wrap">
<div class="eyebrow dark">New-family FAQ</div><h2 class="sec" style="margin-bottom:30px">The questions every new family asks.</h2> <div class="eyebrow dark">New-family FAQ</div><h2 class="sec" style="margin-bottom:30px">The questions every new family asks.</h2>
@@ -526,19 +541,46 @@ def contact_redirect():
@app.post("/join") @app.post("/join")
def join_post(parent_name: str = Form(...), email: str = Form(...), phone: str = Form(""), def join_post(parent_name: str = Form(...), email: str = Form(...), phone: str = Form(""),
children: str = Form(""), program: str = Form(""), message: str = Form("")): children: str = Form(""), interested_in: str = Form(""), source: str = Form(""),
source_place: str = Form(""), source_other: str = Form(""), message: str = Form("")):
if source == "Other school or daycare" and source_place.strip():
source = "School/Daycare: " + source_place.strip()
elif source == "Other" and source_other.strip():
source = "Other: " + source_other.strip()
rec = {"ts": datetime.datetime.now().isoformat(timespec="seconds"), rec = {"ts": datetime.datetime.now().isoformat(timespec="seconds"),
"parent_name": parent_name.strip(), "email": email.strip(), "phone": phone.strip(), "parent_name": parent_name.strip(), "email": email.strip(), "phone": phone.strip(),
"children": children.strip(), "program": program.strip(), "message": message.strip()} "children": children.strip(), "interested_in": interested_in.strip(),
"source": source.strip(), "message": message.strip()}
try: try:
with open(LEADS, "a", encoding="utf-8") as f: with open(LEADS, "a", encoding="utf-8") as f:
f.write(json.dumps(rec) + "\n") f.write(json.dumps(rec) + "\n")
except Exception: except Exception:
pass pass
if NTFY_URL: # Forward into the shared lead pipeline (Google Sheet + ntfy on the 'scouting'
# topic), same as scouting73.thewichersfamily.com. The comment rides along in
# the source column so it reaches the sheet and the phone push.
src = rec["source"] or "greenlanescouts73.org"
if rec["message"]:
src = src + " | Note: " + rec["message"][:300]
forwarded = False
try: try:
b = ("New Scout interest: %s (%s) - %s" % (rec["parent_name"], rec["email"], rec["program"] or "program TBD")).encode() data = urllib.parse.urlencode({
rq = urllib.request.Request(NTFY_URL, data=b, headers={"Title": "greenlanescouts73.org lead", "Tags": "tent"}) "parent_name": rec["parent_name"], "email": rec["email"], "phone": rec["phone"],
"children": rec["children"] or "(not given)",
"interested_in": rec["interested_in"] or "(not given)", "source": src,
}).encode()
rq = urllib.request.Request(FORM_UPSTREAM, data=data)
with urllib.request.urlopen(rq, timeout=6) as resp:
forwarded = resp.status < 400
except Exception:
forwarded = False
if not forwarded and NTFY_URL:
# fallback push so a lead is never silently lost
try:
b = ("New Scout interest: %s (%s) - %s | Kids: %s | %s" % (
rec["parent_name"], rec["email"], rec["interested_in"] or "program TBD",
rec["children"] or "?", src)).encode()
rq = urllib.request.Request(NTFY_URL, data=b, headers={"Title": "greenlanescouts73.org lead (sheet forward FAILED)", "Tags": "tent,warning"})
urllib.request.urlopen(rq, timeout=3) urllib.request.urlopen(rq, timeout=3)
except Exception: except Exception:
pass pass