126 lines
6.3 KiB
Django/Jinja
126 lines
6.3 KiB
Django/Jinja
{# Work request — "request a project quote". A PUBLIC LEAD-GEN FORM.
|
|
|
|
IT LIVES HERE NOW, AND THAT MATTERS. The jesusperez.pro copy of this template exists ONLY in
|
|
`htmx-templates/` — the ASSEMBLED tree, which `just templates` deletes and rebuilds on every run
|
|
(adr-070). It was one command away from vanishing, and that is exactly how the nav submenus were
|
|
lost once. In the framework it is a source file, inherited by every site built from the image.
|
|
|
|
AND ITS SUBMIT WAS DEAD. It posted to `/api/work-request`, a route registered NOWHERE in the
|
|
stack — not in rustelo_server, not in the image, not in the binary. Every submission for as long
|
|
as the page has been up went to an endpoint that does not exist. Nothing looked wrong, because a
|
|
form that RENDERS looks exactly like a form that WORKS.
|
|
|
|
It now posts to the declarative endpoint (`POST /api/htmx/form/work-request`), whose spec is
|
|
`site/config/forms/work-request.ncl`. The spec declares all eleven fields, so all eleven are
|
|
delivered — which is the whole point. Routing this through the four-field contact endpoint would
|
|
have returned 200, said "sent", and mailed the name and email with the description, the budget
|
|
and the timeline silently dropped, because serde ignores unknown fields by default.
|
|
|
|
The field names below MUST match the spec: a posted field the spec does not declare is refused
|
|
with a 400 that names it. That is deliberate — a template and a spec that disagree should fail
|
|
on the first request, not in a quarterly review of an empty inbox. #}
|
|
<article class="ds-page ds-page-work-request" data-page-id="work-request">
|
|
<header class="ds-page-header">
|
|
<div class="ds-container">
|
|
<h1 class="ds-heading-xl">{{ texts["work-request-title"] | default(texts["work-request-page-title"]) }}</h1>
|
|
{% if texts["work-request-subtitle"] %}
|
|
<p class="ds-text-lead">{{ texts["work-request-subtitle"] }}</p>
|
|
{% endif %}
|
|
</div>
|
|
</header>
|
|
|
|
<div class="ds-page-body">
|
|
<div class="ds-container">
|
|
|
|
{% if texts["work-request-description"] %}
|
|
<p class="ds-text-lead">{{ texts["work-request-description"] }}</p>
|
|
{% endif %}
|
|
|
|
<form class="ds-form space-y-ds-4"
|
|
hx-post="/api/htmx/form/work-request"
|
|
hx-target="#work-request-result"
|
|
hx-swap="innerHTML">
|
|
|
|
<input type="hidden" name="lang" value="{{ lang }}" />
|
|
|
|
{# Honeypot. Hidden from people, irresistible to bots; a value here means the handler
|
|
confirms without delivering, so the bot gets no signal. #}
|
|
<div style="position:absolute;left:-9999px" aria-hidden="true">
|
|
<label for="wr-website">Website</label>
|
|
<input id="wr-website" name="website" type="text" tabindex="-1" autocomplete="off" />
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-ds-4">
|
|
<div>
|
|
<label for="wr-name" class="ds-label">{{ texts["work-request-your-name"] }}</label>
|
|
<input id="wr-name" name="name" type="text" required class="ds-input w-full"
|
|
placeholder="{{ texts['work-request-name-placeholder'] }}" />
|
|
</div>
|
|
<div>
|
|
<label for="wr-email" class="ds-label">{{ texts["work-request-your-email"] }}</label>
|
|
<input id="wr-email" name="email" type="email" required class="ds-input w-full"
|
|
placeholder="{{ texts['work-request-email-placeholder'] }}" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-ds-4">
|
|
<div>
|
|
<label for="wr-company" class="ds-label">{{ texts["work-request-company"] }}</label>
|
|
<input id="wr-company" name="company" type="text" class="ds-input w-full" />
|
|
</div>
|
|
<div>
|
|
<label for="wr-project-type" class="ds-label">{{ texts["work-request-project-type-label"] }}</label>
|
|
<select id="wr-project-type" name="project_type" class="ds-input w-full">
|
|
<option value=""></option>
|
|
{% for opt in ["web", "infrastructure", "consulting", "other"] %}
|
|
<option value="{{ opt }}">{{ texts["work-request-project-type-label-" ~ opt] | default(opt) }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="wr-description" class="ds-label">{{ texts["work-request-project-description-label"] }}</label>
|
|
<textarea id="wr-description" name="project_description" rows="5" required class="ds-input w-full"></textarea>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="wr-situation" class="ds-label">{{ texts["work-request-current-situation-label"] }}</label>
|
|
<textarea id="wr-situation" name="current_situation" rows="3" class="ds-input w-full"></textarea>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="wr-goals" class="ds-label">{{ texts["work-request-goals-label"] }}</label>
|
|
<textarea id="wr-goals" name="goals" rows="3" class="ds-input w-full"></textarea>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="wr-technical" class="ds-label">{{ texts["work-request-technical-requirements-label"] }}</label>
|
|
<textarea id="wr-technical" name="technical_requirements" rows="3" class="ds-input w-full"></textarea>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-ds-4">
|
|
<div>
|
|
<label for="wr-timeline" class="ds-label">{{ texts["work-request-timeline-label"] }}</label>
|
|
<input id="wr-timeline" name="timeline" type="text" class="ds-input w-full" />
|
|
</div>
|
|
<div>
|
|
<label for="wr-budget" class="ds-label">{{ texts["work-request-budget"] }}</label>
|
|
<input id="wr-budget" name="budget" type="text" class="ds-input w-full" />
|
|
</div>
|
|
<div>
|
|
<label for="wr-contact-method" class="ds-label">{{ texts["work-request-contact-method-label"] }}</label>
|
|
<input id="wr-contact-method" name="contact_method" type="text" class="ds-input w-full" />
|
|
</div>
|
|
</div>
|
|
|
|
<button type="submit" class="ds-btn ds-btn-primary">
|
|
{{ texts["work-request-submit"] | default(texts["work-request-send"]) | default("Send") }}
|
|
</button>
|
|
|
|
<div id="work-request-result" role="status" aria-live="polite"></div>
|
|
</form>
|
|
|
|
</div>
|
|
</div>
|
|
</article>
|