Initial commit 🚀
This commit is contained in:
commit
fd58a8d32f
235 files changed
+19775
No files matched your search
@@ -0,0 +1,125 @@
|
||||
---
|
||||
import Toast from '../Toast.astro';
|
||||
import { t, type Lang } from '../../i18n/ui';
|
||||
interface Props { lang: Lang }
|
||||
const { lang } = Astro.props;
|
||||
const ui = t(lang);
|
||||
const f = ui.contact.form;
|
||||
---
|
||||
<section class="section contact" id="contact" aria-labelledby="contact-title">
|
||||
<div class="container grid">
|
||||
<div class="contact__text">
|
||||
<h2 id="contact-title" class="h2">{ui.contact.title}</h2>
|
||||
<p class="contact__body">{ui.contact.body}</p>
|
||||
<address class="contact__address">
|
||||
<p>{ui.contact.address[0]}<br />{ui.contact.address[1]}</p>
|
||||
<p><a href={ui.contact.phoneHref}>{ui.contact.phone}</a><br /><a href={`mailto:${ui.contact.email}`}>{ui.contact.email}</a></p>
|
||||
</address>
|
||||
</div>
|
||||
|
||||
<form
|
||||
class="contact__form"
|
||||
id="contact-form"
|
||||
action="/contact"
|
||||
method="post"
|
||||
lang={lang}
|
||||
data-contact
|
||||
data-sent={f.sent}
|
||||
data-invalid={f.invalid}
|
||||
data-failed={f.failed}
|
||||
>
|
||||
<input type="hidden" name="lang" value={lang} />
|
||||
<p class="visually-hidden" aria-hidden="true">
|
||||
<label for="website">{f.honeypot}</label>
|
||||
<input type="text" id="website" name="website" tabindex="-1" autocomplete="off" />
|
||||
</p>
|
||||
<div class="field">
|
||||
<label for="first_name">{f.firstName} <span aria-hidden="true">*</span></label>
|
||||
<input id="first_name" name="first_name" type="text" autocomplete="given-name" required />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="last_name">{f.lastName} <span aria-hidden="true">*</span></label>
|
||||
<input id="last_name" name="last_name" type="text" autocomplete="family-name" required />
|
||||
</div>
|
||||
<div class="field field--full">
|
||||
<label for="email">{f.email} <span aria-hidden="true">*</span></label>
|
||||
<input id="email" name="email" type="email" autocomplete="email" required />
|
||||
</div>
|
||||
<div class="field field--full">
|
||||
<label for="message">{f.message} <span aria-hidden="true">*</span></label>
|
||||
<textarea id="message" name="message" rows="5" required></textarea>
|
||||
</div>
|
||||
<div class="field--full contact__actions">
|
||||
<button class="button" type="submit">{f.submit}</button>
|
||||
<p class="small muted"><span aria-hidden="true">* </span>{f.required}</p>
|
||||
</div>
|
||||
<noscript><p class="field--full small">{f.noscript}</p></noscript>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<Toast for="contact-form" labels={{ ...f.toast }} />
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.contact { border-top: var(--hairline) solid var(--line); }
|
||||
.contact__text {
|
||||
grid-column: 1 / -1;
|
||||
display: grid;
|
||||
gap: var(--space-6);
|
||||
align-content: start;
|
||||
}
|
||||
.contact__body { max-width: 28em; color: var(--ink-muted); }
|
||||
.contact__address { font-style: normal; display: grid; gap: var(--space-4); }
|
||||
.contact__address a { text-decoration: none; }
|
||||
.contact__address a:hover { text-decoration: underline; }
|
||||
|
||||
.contact__form {
|
||||
grid-column: 1 / -1;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: var(--space-6) var(--gap);
|
||||
align-content: start;
|
||||
}
|
||||
.field { display: grid; gap: var(--space-2); }
|
||||
.field--full { grid-column: 1 / -1; }
|
||||
.field label { font-size: var(--step-small); line-height: var(--leading-small); color: var(--ink-muted); }
|
||||
.field input, .field textarea {
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-bottom: var(--hairline) solid var(--line);
|
||||
padding: var(--space-2) 0 var(--space-3);
|
||||
border-radius: 0;
|
||||
transition: border-color var(--dur-fast) var(--ease);
|
||||
}
|
||||
.field input:focus-visible, .field textarea:focus-visible {
|
||||
border-bottom-color: currentColor;
|
||||
}
|
||||
.field textarea { resize: vertical; min-height: 8rem; }
|
||||
.contact__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: var(--space-5);
|
||||
margin-top: var(--space-2);
|
||||
}
|
||||
|
||||
.contact__form[aria-busy='true'] .button { opacity: 0.6; cursor: progress; }
|
||||
|
||||
@media (min-width: 48rem) {
|
||||
.contact__text { grid-column: 1 / span 5; }
|
||||
.contact__form { grid-column: 7 / span 6; }
|
||||
}
|
||||
@media (max-width: 30rem) {
|
||||
.contact__form { grid-template-columns: 1fr; }
|
||||
.field { grid-column: 1 / -1; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Sends in place and answers in the toast; see scripts/send-form.ts. Without scripting the
|
||||
// server refuses the post and redirects to /contact/error, which points to the email address.
|
||||
import { sendInPlace } from '../../scripts/send-form';
|
||||
const form = document.querySelector<HTMLFormElement>('[data-contact]');
|
||||
if (form) sendInPlace(form);
|
||||
</script>
|
||||
Reference in new issue
Block a user