> Source URL: /unit-3/project-paths/lucas-w/lucas-w-2026-04-23.guide
# Lucas's Project Guide

**Project:** Roof Inspection Lead Generation Page
**Category:** Web Development (Flask)
**Last updated:** April 23

---

> Note: This guide reflects the latest state of your project repo. It may not match the most up-to-date version if you've worked since.

## Phase 1: Form layout + mobile-friendly inputs

> **Agent-assisted OK.** Pure view/markup. No business logic changes.

### Objective

Cap the form width on desktop so it stops looking like a stretched table, and use the right input types/attributes so mobile browsers show the correct keyboard and offer autofill.

### Instructions

- [ ] In `templates/home.html`, wrap the `<div class="card shadow">` in a Bootstrap row+column that caps width on large screens
- [ ] Apply the same wrapper to `templates/confirmation.html` so both pages match
- [ ] Change `phone` to `type="tel"` and add `autocomplete` + `inputmode` on every field
- [ ] Test on your phone (or Chrome DevTools responsive mode): the email field should pop up the `@` keyboard, the phone field the number pad, and iOS should offer to autofill name/address from your contact card

### Hints

**Width wrapper pattern (drop this around the existing `.card`):**

```html
<div class="container py-5">
  <div class="row justify-content-center">
    <div class="col-md-8 col-lg-6">
      <div class="card shadow">
        <!-- existing card-body stays as-is -->
      </div>
    </div>
  </div>
</div>
```

`col-md-8` = 8/12 width on tablets, `col-lg-6` = 6/12 on desktop. Phones stay full-width automatically. `justify-content-center` centers the column in the row.

**Updated input pattern (apply the matching attributes to each field):**

```html
<div class="mb-3">
  <label for="name" class="form-label">Full Name</label>
  <input
    id="name"
    name="name"
    type="text"
    class="form-control"
    autocomplete="name"
    required
  />
</div>

<div class="mb-3">
  <label for="phone" class="form-label">Phone Number</label>
  <input
    id="phone"
    name="phone"
    type="tel"
    class="form-control"
    autocomplete="tel"
    inputmode="tel"
    required
  />
</div>

<div class="mb-3">
  <label for="email" class="form-label">Email</label>
  <input
    id="email"
    name="email"
    type="email"
    class="form-control"
    autocomplete="email"
    inputmode="email"
    required
  />
</div>

<div class="mb-3">
  <label for="address" class="form-label">Address</label>
  <input
    id="address"
    name="address"
    type="text"
    class="form-control"
    autocomplete="street-address"
    required
  />
</div>

<div class="mb-3">
  <label for="insurance" class="form-label">Home Insurance Provider</label>
  <input
    id="insurance"
    name="insurance"
    type="text"
    class="form-control"
    autocomplete="organization"
    required
  />
</div>
```

**Why these attributes matter:**

- `type="tel"` / `type="email"` → the phone keyboard is numbers-first; the email keyboard has `@` and `.` on the main row.
- `autocomplete="..."` → iOS/Android offer to fill the field from the user's contact card or saved autofill. Single biggest mobile UX win you can add in one line.
- `inputmode="..."` → hints to the browser which keyboard to show even when `type="text"`.

> **Optional — get help from your agent:**
>
> ```text
> Take my home.html and confirmation.html. Wrap the card in a
> centered column that caps at col-lg-6. Don't change any input
> names or ids — app.py reads them by name. Also add the
> autocomplete and inputmode attributes from the guide.
> ```

---

## Phase 2: Useful confirmation screen

### Objective

Right now `confirmation.html` just says "Thank you, Lucas!" When someone prints it, they get a receipt with no detail. Show them every field they submitted so the printed page is actually useful.

### Instructions

- [ ] In `app.py`, build a `lead` dict inside the POST branch and pass the whole dict to `save_lead(**lead)` and `render_template("confirmation.html", lead=lead)`
- [ ] In `templates/confirmation.html`, render each field under a heading
- [ ] Keep the print button and "Submit another" link

### Hints

**Refactor the POST branch of your `home` route:**

```python
if request.method == "POST":
    lead = {
        "name": request.form.get("name", "").strip(),
        "phone": request.form.get("phone", "").strip(),
        "email": request.form.get("email", "").strip(),
        "address": request.form.get("address", "").strip(),
        "insurance": request.form.get("insurance", "").strip(),
        "reason": request.form.get("reason", "").strip(),
    }

    if not all(lead.values()):
        return render_template(
            "home.html",
            title="Roof Inspection Form",
            error="Please fill out every field.",
        )

    save_lead(**lead)
    return render_template("confirmation.html", lead=lead)
```

`save_lead(**lead)` unpacks the dict into keyword arguments. Your existing `save_lead(name, phone, email, address, insurance, reason)` signature already matches the dict keys, so nothing in `logic.py` changes.

**In `confirmation.html` (inside the card body):**

```html
<h1 class="card-title mb-3">Thank you, {{ lead.name }}!</h1>
<p class="text-muted mb-4">
  Your roof inspection request has been submitted successfully. We'll be in
  touch at {{ lead.phone }} or {{ lead.email }}.
</p>

<dl class="row text-start mb-4">
  <dt class="col-sm-4">Name</dt>
  <dd class="col-sm-8">{{ lead.name }}</dd>

  <dt class="col-sm-4">Phone</dt>
  <dd class="col-sm-8">{{ lead.phone }}</dd>

  <dt class="col-sm-4">Email</dt>
  <dd class="col-sm-8">{{ lead.email }}</dd>

  <dt class="col-sm-4">Address</dt>
  <dd class="col-sm-8">{{ lead.address }}</dd>

  <dt class="col-sm-4">Insurance</dt>
  <dd class="col-sm-8">{{ lead.insurance }}</dd>

  <dt class="col-sm-4">Reason</dt>
  <dd class="col-sm-8">{{ lead.reason }}</dd>
</dl>
```

`<dl>` / `<dt>` / `<dd>` is the right HTML for label/value pairs — literally called a "description list."

> **Optional — get help from your agent:**
>
> Skip — this is straightforward Jinja.

---

## Phase 3: One-click follow-up mailto links (Stretch Goal 2)

> **Agent-assisted OK.** This is HTML/frontend code.

### Objective

From `/owner/leads` you can see every lead. Now add a **Follow up** button per row that opens a pre-filled email draft in your default mail client (Gmail in the browser, Apple Mail, Outlook — whatever's set). This covers your Stretch Goal 2 with zero SMTP setup.

### Why `mailto:` instead of an SMTP send route?

Your original Stretch Goal 2 spec said "send a follow-up email from the dashboard." `mailto:` is the simpler version — the user's own email client opens with `to`, `subject`, and `body` pre-filled, and they can edit/send from there. For outbound sales-style follow-ups this is actually what you want: the reply lands in your sent folder and threads with any response. No API keys, no deliverability headaches.

### Instructions

- [ ] In `templates/owner_leads.html`, add a new "Follow up" column to the table header
- [ ] For each row, render a `mailto:` link with pre-filled `subject` and `body` using Jinja's `urlencode` filter
- [ ] Tweak the template copy in your own voice (first thing a real lead hears from you)

### Hints

**Add to the `<thead>` `<tr>`, after `<th>Reason</th>`:**

```html
<th>Follow up</th>
```

**Add as the last `<td>` inside the `{% for lead in leads %}` loop** (your leads are `sqlite3.Row` objects, so keep using `lead["field"]` to match the rest of your template):

```html
<td>
  {% set subject = "Your roof inspection request" %} {% set body %} Hi {{
  lead["name"] }}, Thanks for requesting a roof inspection at {{ lead["address"]
  }}. I'd love to set up a time to come take a look. A few quick questions to
  confirm: - Best time to reach you at {{ lead["phone"] }}? - Is {{
  lead["insurance"] }} still your current home insurance? - You mentioned: "{{
  lead["reason"] }}" — anything else I should know before I come out? Talk soon,
  Lucas {% endset %}
  <a
    class="btn btn-sm btn-primary"
    href="mailto:{{ lead['email'] }}?subject={{ subject|urlencode }}&body={{ body|urlencode }}"
  >
    Follow up
  </a>
</td>
```

A few things worth understanding:

- `{% set body %}...{% endset %}` lets you assign a multi-line string in Jinja.
- `|urlencode` converts spaces, newlines, and special characters into the `%20`-style encoding `mailto:` needs. Without it, a newline or quote breaks the link.
- `mailto:...?subject=...&body=...` is a standard URL. The default email client opens with a pre-filled draft; you can still edit before hitting send.

**Test it:** visit `/owner/leads?key=YOUR_KEY`, click a Follow up button. Your email client should open with the lead's email in To, your subject, and the templated body ready to edit.

> **Optional — get help from your agent:**
>
> ```text
> In my owner_leads.html, add a Follow up column. Each cell is a
> Bootstrap button that opens a mailto: link with the lead's email
> pre-filled, my subject "Your roof inspection request", and a
> multi-line body that references the lead's name, address, phone,
> insurance, and reason. Use Jinja's urlencode filter. My template
> uses lead["field"] syntax — match that.
> ```

---

## Phase 4: Auto email notification on new lead (Stretch Goal 3, via Resend)

> **Mixed.** The API call is library work. The email copy is yours.

### Objective

When a new lead submits the form, you get an email. No dashboard refreshes required. [Resend](https://resend.com) is a modern transactional email service with a generous free tier (3,000 emails/month) and a clean Python SDK — easier than wiring up Gmail SMTP.

### Instructions

- [ ] Sign up at [resend.com](https://resend.com) and create an API key
- [ ] For local dev, use their test sender `onboarding@resend.dev` (no domain verification needed)
- [ ] Add `resend` to your dependencies: `uv add resend`
- [ ] Extend your existing `.env` with `RESEND_API_KEY`, `OWNER_EMAIL`, `SENDER_EMAIL`
- [ ] Create `.env.example` documenting the keys (empty values, committed)
- [ ] Create `notifications.py` with a `notify_owner(lead)` function
- [ ] In `app.py`, call `notify_owner(lead)` after `save_lead(**lead)` inside a `try/except` so email failures don't break form submissions

### Hints

**Install:**

```bash
uv add resend
```

(You already have `python-dotenv` — no need to add it again.)

**`.env` (NEVER commit — `.env` is already in your `.gitignore`, good):**

```text
OWNER_KEY=your_existing_owner_key
RESEND_API_KEY=re_your_key_here
OWNER_EMAIL=lucas@example.com
SENDER_EMAIL=onboarding@resend.dev
```

**`.env.example` (commit this so anyone cloning the repo knows what to set):**

```text
OWNER_KEY=
RESEND_API_KEY=
OWNER_EMAIL=
SENDER_EMAIL=onboarding@resend.dev
```

**`notifications.py` (new file at project root):**

```python
"""Owner notifications — sends an email when a new lead arrives.

Keeps email concerns out of logic.py (DB) and app.py (routes).
"""

import os
import resend

resend.api_key = os.environ.get("RESEND_API_KEY")


def notify_owner(lead):
    owner = os.environ.get("OWNER_EMAIL")
    sender = os.environ.get("SENDER_EMAIL", "onboarding@resend.dev")

    if not resend.api_key or not owner:
        print("[notify_owner] Skipping — RESEND_API_KEY or OWNER_EMAIL not set.")
        return

    body = f"""New roof inspection lead:

Name:      {lead['name']}
Phone:     {lead['phone']}
Email:     {lead['email']}
Address:   {lead['address']}
Insurance: {lead['insurance']}
Reason:    {lead['reason']}
"""

    resend.Emails.send({
        "from": sender,
        "to": [owner],
        "subject": f"New lead: {lead['name']}",
        "text": body,
    })
```

Note: no `load_dotenv()` call here — `app.py` already calls `load_dotenv()` at import time, so env vars are populated before `notifications` is imported.

**In `app.py`, add the import and the call:**

```python
from notifications import notify_owner

# ...inside the POST branch of home(), after save_lead(**lead):
try:
    notify_owner(lead)
except Exception as e:
    print(f"[app] Email notification failed: {e}")

return render_template("confirmation.html", lead=lead)
```

The `try/except` matches your stretch-goals-steps Goal 3: "Wrap email send in try/except so submission still succeeds if email fails." You'd rather miss an email than drop a lead.

**Test it:**

1. Fill out the form at `/` locally.
2. Watch your terminal — you should see no errors.
3. Check the inbox at `OWNER_EMAIL`. If it's not there, check spam and check the terminal for exception text.

> **Optional — get help from your agent:**
>
> ```text
> Walk me through how load_dotenv() in app.py makes os.environ.get()
> work in notifications.py. Why don't I need to call load_dotenv()
> in both files? What happens on Render where there's no .env file?
> ```

---

## Phase 5 (Bonus): Deploy to Render

> **Only attempt after Phases 1–4 work locally.** Cherry on top — skip if short on time.

### Objective

Put your app on the internet at a URL like `https://roof-lead-gen.onrender.com` so your demo pulls up a real live site instead of `localhost`. Corresponds to your Stretch Goal 6.

### Instructions

- [ ] Read the full [Render Deploy Guide](../../resources/render-deploy.guide.md) — covers the click-through in detail
- [ ] Run `uv add gunicorn` so the production server is listed in `pyproject.toml` and `uv.lock`
- [ ] Create `render.yaml` at project root with `envVars` for every secret
- [ ] Commit `pyproject.toml`, `uv.lock`, and `render.yaml`
- [ ] Push, create a Render account, connect your repo
- [ ] In the Render dashboard, paste actual values for `OWNER_KEY`, `RESEND_API_KEY`, `OWNER_EMAIL`, `SENDER_EMAIL`
- [ ] Test the live URL end-to-end: submit a lead, check your email, visit `/owner/leads?key=...`
- [ ] Delete `main.py` (unused placeholder)

### Hints

**`render.yaml`:**

```yaml
services:
  - type: web
    name: roof-lead-gen
    env: python
    plan: free
    buildCommand: uv sync --frozen && uv cache prune --ci
    startCommand: uv run gunicorn app:app
    autoDeploy: true
    envVars:
      - key: OWNER_KEY
        sync: false
      - key: RESEND_API_KEY
        sync: false
      - key: OWNER_EMAIL
        sync: false
      - key: SENDER_EMAIL
        sync: false
```

`sync: false` means "don't bake the value into git — I'll set it in the Render dashboard." That's how your keys stay out of the repo.

**Important — SQLite caveat for demo day:**

Render's free tier wipes the disk on every deploy. Your `data/leads.db` will reset every time you push code or Render restarts your service. For demo day this is fine — submit 2–3 fresh test leads right before your presentation so `/owner/leads` isn't empty. Don't treat the live site as a real CRM yet.

If you ever want persistent storage, see the Render Disks or hosted Postgres section of the deploy guide.

> **Optional — get help from your agent:**
>
> ```text
> I've deployed to Render using the guide. The build succeeded but
> visiting /owner/leads?key=... returns 403 even though I'm using
> the right key. Walk me through checking the OWNER_KEY env var in
> the Render dashboard and how to see the live logs.
> ```

---

## Checkpoint 3 Readiness

By **Friday, May 1st at 3:30pm (Demo Day)**:

- [ ] Form is width-capped on desktop, uses proper input types + `autocomplete` on mobile
- [ ] Confirmation page shows every submitted field (useful to print)
- [ ] `/owner/leads` has a working "Follow up" `mailto:` button per row
- [ ] New form submissions trigger a Resend email to your inbox
- [ ] `.env` contains `OWNER_KEY`, `RESEND_API_KEY`, `OWNER_EMAIL`, `SENDER_EMAIL`; `.env.example` committed; `.env` still gitignored
- [ ] `main.py` deleted (or repurposed — it's currently dead code)
- [ ] README updated: one-paragraph description, run commands, short list of env vars needed, a screenshot or GIF
- [ ] Checkpoint 3 entry in `project.journal.md` (what you built, what you wrote yourself vs. agent-assisted, what you'd do next)
- [ ] Committed and pushed to GitHub
- [ ] (Bonus) Live Render URL working end-to-end
- [ ] 2–5 minute demo plan sketched in your journal

## Helpful Resources

- [Checkpoint 3 Instructions](../../projects/final-project-checkpoint-3.project.md)
- [Render Deploy Guide](../../resources/render-deploy.guide.md)
- [Resend Python SDK docs](https://resend.com/docs/send-with-python)
- [Bootstrap 5 Grid](https://getbootstrap.com/docs/5.3/layout/grid/)
- [MDN: `autocomplete` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete)
- [MDN: `mailto:` links](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#mailto_links)


---

## Backlinks

The following sources link to this document:

- [April 23 — Checkpoint 3 (Polish + Follow-up Workflow)](/unit-3/project-paths/projects.path.llm.md)
- [April 23 -- Checkpoint 3 (Polish + Follow-up Workflow)](/unit-3/project-paths/lucas-w/lucas-w.path.llm.md)
- [April 23 - Checkpoint 3](/unit-3/projects/showcase/lucas-w.project.llm.md)
