site: multi-page structure with shared nav; rework services, keys, contact
This commit is contained in:
parent
03f38ab908
commit
82fc1c6e17
12 changed files with 143 additions and 151 deletions
35
asxpio.rb
35
asxpio.rb
|
|
@ -89,12 +89,25 @@ class AsxpioWeb < Sinatra::Base
|
||||||
end
|
end
|
||||||
|
|
||||||
get '/' do
|
get '/' do
|
||||||
@form_errors = nil
|
|
||||||
@form_values = {}
|
|
||||||
erb :index
|
erb :index
|
||||||
end
|
end
|
||||||
|
|
||||||
|
get '/keys' do
|
||||||
|
@page_title = 'Keys · Sergei Poljanski'
|
||||||
|
@page_desc = 'PGP fingerprint and SSH public key of Sergei Poljanski. Verify or download the key files.'
|
||||||
|
erb :keys
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/contact' do
|
||||||
|
@page_title = 'Contact · Sergei Poljanski'
|
||||||
|
@page_desc = 'Contact Sergei Poljanski: contact form, email, Telegram, phone. Legal entity details for invoices and contracts.'
|
||||||
|
@form_errors = nil
|
||||||
|
@form_values = {}
|
||||||
|
erb :contact
|
||||||
|
end
|
||||||
|
|
||||||
post '/contact' do
|
post '/contact' do
|
||||||
|
@page_title = 'Contact · Sergei Poljanski'
|
||||||
name = params[:name].to_s.strip
|
name = params[:name].to_s.strip
|
||||||
email = params[:email].to_s.strip
|
email = params[:email].to_s.strip
|
||||||
subject = params[:subject].to_s.strip
|
subject = params[:subject].to_s.strip
|
||||||
|
|
@ -114,13 +127,13 @@ class AsxpioWeb < Sinatra::Base
|
||||||
|
|
||||||
if @form_errors.any?
|
if @form_errors.any?
|
||||||
status 422
|
status 422
|
||||||
return erb :index
|
return erb :contact
|
||||||
end
|
end
|
||||||
|
|
||||||
unless RATE_LIMIT.allow?(client_ip)
|
unless RATE_LIMIT.allow?(client_ip)
|
||||||
@form_errors[:base] = 'Too many submissions. Try again later or email ie@asxp.io directly.'
|
@form_errors[:base] = 'Too many submissions. Try again later or email ie@asxp.io directly.'
|
||||||
status 429
|
status 429
|
||||||
return erb :index
|
return erb :contact
|
||||||
end
|
end
|
||||||
|
|
||||||
begin
|
begin
|
||||||
|
|
@ -131,7 +144,7 @@ class AsxpioWeb < Sinatra::Base
|
||||||
$logger.error("notify_owner failed: #{e.class}: #{e.message}")
|
$logger.error("notify_owner failed: #{e.class}: #{e.message}")
|
||||||
@form_errors[:base] = 'Could not send message right now. Please email ie@asxp.io directly.'
|
@form_errors[:base] = 'Could not send message right now. Please email ie@asxp.io directly.'
|
||||||
status 500
|
status 500
|
||||||
return erb :index
|
return erb :contact
|
||||||
end
|
end
|
||||||
|
|
||||||
begin
|
begin
|
||||||
|
|
@ -156,7 +169,7 @@ class AsxpioWeb < Sinatra::Base
|
||||||
end
|
end
|
||||||
|
|
||||||
get '/thanks' do
|
get '/thanks' do
|
||||||
@page_title = 'Thanks — IE Sergei Poljanski'
|
@page_title = 'Thanks · IE Sergei Poljanski'
|
||||||
@page_desc = 'Your message has been received. A confirmation copy has been sent to your inbox.'
|
@page_desc = 'Your message has been received. A confirmation copy has been sent to your inbox.'
|
||||||
@noindex = true
|
@noindex = true
|
||||||
erb :thanks
|
erb :thanks
|
||||||
|
|
@ -177,7 +190,7 @@ class AsxpioWeb < Sinatra::Base
|
||||||
end
|
end
|
||||||
|
|
||||||
get '/admin/invoices' do
|
get '/admin/invoices' do
|
||||||
@page_title = 'Invoices — admin'
|
@page_title = 'Invoices · admin'
|
||||||
@noindex = true
|
@noindex = true
|
||||||
@invoices = Invoice.order(Sequel.desc(:created_at)).all
|
@invoices = Invoice.order(Sequel.desc(:created_at)).all
|
||||||
erb :'admin/invoices/index'
|
erb :'admin/invoices/index'
|
||||||
|
|
@ -225,7 +238,7 @@ class AsxpioWeb < Sinatra::Base
|
||||||
end
|
end
|
||||||
|
|
||||||
get '/admin/invoices/new' do
|
get '/admin/invoices/new' do
|
||||||
@page_title = 'New invoice — admin'
|
@page_title = 'New invoice · admin'
|
||||||
@noindex = true
|
@noindex = true
|
||||||
@form_errors = nil
|
@form_errors = nil
|
||||||
@form_values = {}
|
@form_values = {}
|
||||||
|
|
@ -240,7 +253,7 @@ class AsxpioWeb < Sinatra::Base
|
||||||
get '/admin/invoices/:uuid/duplicate' do
|
get '/admin/invoices/:uuid/duplicate' do
|
||||||
src = Invoice[uuid: params[:uuid]] or halt 404
|
src = Invoice[uuid: params[:uuid]] or halt 404
|
||||||
term = (src.due_on - src.issued_on).to_i
|
term = (src.due_on - src.issued_on).to_i
|
||||||
@page_title = 'New invoice — admin'
|
@page_title = 'New invoice · admin'
|
||||||
@noindex = true
|
@noindex = true
|
||||||
@form_errors = nil
|
@form_errors = nil
|
||||||
@form_values = {
|
@form_values = {
|
||||||
|
|
@ -278,7 +291,7 @@ class AsxpioWeb < Sinatra::Base
|
||||||
@form_errors = validate_invoice_params(@form_values)
|
@form_errors = validate_invoice_params(@form_values)
|
||||||
|
|
||||||
if @form_errors.any?
|
if @form_errors.any?
|
||||||
@page_title = 'New invoice — admin'
|
@page_title = 'New invoice · admin'
|
||||||
@noindex = true
|
@noindex = true
|
||||||
status 422
|
status 422
|
||||||
return erb :'admin/invoices/new'
|
return erb :'admin/invoices/new'
|
||||||
|
|
@ -309,7 +322,7 @@ class AsxpioWeb < Sinatra::Base
|
||||||
|
|
||||||
get '/admin/invoices/:uuid' do
|
get '/admin/invoices/:uuid' do
|
||||||
@invoice = Invoice[uuid: params[:uuid]] or halt 404
|
@invoice = Invoice[uuid: params[:uuid]] or halt 404
|
||||||
@page_title = "#{@invoice.number} — admin"
|
@page_title = "#{@invoice.number} · admin"
|
||||||
@noindex = true
|
@noindex = true
|
||||||
erb :'admin/invoices/show'
|
erb :'admin/invoices/show'
|
||||||
end
|
end
|
||||||
|
|
|
||||||
21
public/copy.js
Normal file
21
public/copy.js
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
// One copy button per [data-copy] element. Copies the data-copy value when
|
||||||
|
// set, otherwise the element's original text. Clipboard API needs a secure
|
||||||
|
// context — https in prod, localhost in dev; both hold.
|
||||||
|
document.querySelectorAll('[data-copy]').forEach(function (el) {
|
||||||
|
var text = el.getAttribute('data-copy') || el.textContent.trim();
|
||||||
|
|
||||||
|
var btn = document.createElement('span');
|
||||||
|
btn.innerText = 'Copy';
|
||||||
|
btn.classList.add('copy-button');
|
||||||
|
el.appendChild(btn);
|
||||||
|
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
navigator.clipboard.writeText(text).then(function () {
|
||||||
|
btn.innerText = 'Copied!';
|
||||||
|
setTimeout(function () { btn.innerText = 'Copy'; }, 2000);
|
||||||
|
}, function () {
|
||||||
|
btn.innerText = 'Failed';
|
||||||
|
setTimeout(function () { btn.innerText = 'Copy'; }, 2000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
// Get the <pre> element
|
|
||||||
const PGP = document.getElementById("myPGP");
|
|
||||||
|
|
||||||
//Create a copy button element
|
|
||||||
const copyButton = document.createElement("span");
|
|
||||||
copyButton.innerText = "Copy";
|
|
||||||
copyButton.classList.add("copy-button");
|
|
||||||
|
|
||||||
// Append the copy button to the <pre> tag
|
|
||||||
PGP.appendChild(copyButton);
|
|
||||||
|
|
||||||
// Add click event listener to the copy button
|
|
||||||
copyButton.addEventListener("click", () => {
|
|
||||||
// Hide the copy button temporarily
|
|
||||||
copyButton.style.display = "none";
|
|
||||||
|
|
||||||
// Create a range and select the text inside the <pre> tag
|
|
||||||
const range = document.createRange();
|
|
||||||
range.selectNode(PGP);
|
|
||||||
window.getSelection().removeAllRanges();
|
|
||||||
window.getSelection().addRange(range);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Copy the selected text to the clipboard
|
|
||||||
document.execCommand("copy");
|
|
||||||
|
|
||||||
// Alert the user that the text has been copied
|
|
||||||
copyButton.innerText = "Copied!";
|
|
||||||
setTimeout(function(){
|
|
||||||
copyButton.innerText = "Copy";
|
|
||||||
}, 2000);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Unable to copy text:", err);
|
|
||||||
} finally {
|
|
||||||
// Show the copy button again
|
|
||||||
copyButton.style.display = "inline";
|
|
||||||
|
|
||||||
// Deselect the text
|
|
||||||
window.getSelection().removeAllRanges();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
// Get the <pre> element
|
|
||||||
const SSH = document.getElementById("mySSH");
|
|
||||||
|
|
||||||
//Create a copy button element
|
|
||||||
const copyButtonSSH = document.createElement("span");
|
|
||||||
copyButtonSSH.innerText = "Copy";
|
|
||||||
copyButtonSSH.classList.add("copy-button");
|
|
||||||
|
|
||||||
// Append the copy button to the <pre> tag
|
|
||||||
SSH.appendChild(copyButtonSSH);
|
|
||||||
|
|
||||||
// Add click event listener to the copy button
|
|
||||||
copyButtonSSH.addEventListener("click", () => {
|
|
||||||
// Hide the copy button temporarily
|
|
||||||
copyButtonSSH.style.display = "none";
|
|
||||||
|
|
||||||
// Create a range and select the text inside the <pre> tag
|
|
||||||
const range = document.createRange();
|
|
||||||
range.selectNode(SSH);
|
|
||||||
window.getSelection().removeAllRanges();
|
|
||||||
window.getSelection().addRange(range);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Copy the selected text to the clipboard
|
|
||||||
document.execCommand("copy");
|
|
||||||
|
|
||||||
// Alert the user that the text has been copied
|
|
||||||
copyButtonSSH.innerText = "Copied!";
|
|
||||||
setTimeout(function(){
|
|
||||||
copyButtonSSH.innerText = "Copy";
|
|
||||||
}, 2000);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Unable to copy text:", err);
|
|
||||||
} finally {
|
|
||||||
// Show the copy button again
|
|
||||||
copyButtonSSH.style.display = "inline";
|
|
||||||
|
|
||||||
// Deselect the text
|
|
||||||
window.getSelection().removeAllRanges();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -5,4 +5,14 @@
|
||||||
<changefreq>monthly</changefreq>
|
<changefreq>monthly</changefreq>
|
||||||
<priority>1.0</priority>
|
<priority>1.0</priority>
|
||||||
</url>
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://asxp.io/contact</loc>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://asxp.io/keys</loc>
|
||||||
|
<changefreq>yearly</changefreq>
|
||||||
|
<priority>0.5</priority>
|
||||||
|
</url>
|
||||||
</urlset>
|
</urlset>
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ class AppTest < Minitest::Test
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_contact_honeypot_pretends_success_and_sends_nothing
|
def test_contact_honeypot_pretends_success_and_sends_nothing
|
||||||
token = csrf_token_from('/')
|
token = csrf_token_from('/contact')
|
||||||
post '/contact', contact_params(website: 'spam', authenticity_token: token)
|
post '/contact', contact_params(website: 'spam', authenticity_token: token)
|
||||||
assert_equal 302, last_response.status
|
assert_equal 302, last_response.status
|
||||||
assert_match %r{/thanks}, last_response.location
|
assert_match %r{/thanks}, last_response.location
|
||||||
|
|
@ -67,14 +67,14 @@ class AppTest < Minitest::Test
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_contact_invalid_email_rejected
|
def test_contact_invalid_email_rejected
|
||||||
token = csrf_token_from('/')
|
token = csrf_token_from('/contact')
|
||||||
post '/contact', contact_params(email: 'not-an-email', authenticity_token: token)
|
post '/contact', contact_params(email: 'not-an-email', authenticity_token: token)
|
||||||
assert_equal 422, last_response.status
|
assert_equal 422, last_response.status
|
||||||
assert_empty Mail::TestMailer.deliveries
|
assert_empty Mail::TestMailer.deliveries
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_contact_valid_submission_sends_two_mails
|
def test_contact_valid_submission_sends_two_mails
|
||||||
token = csrf_token_from('/')
|
token = csrf_token_from('/contact')
|
||||||
post '/contact', contact_params(authenticity_token: token),
|
post '/contact', contact_params(authenticity_token: token),
|
||||||
'HTTP_X_FORWARDED_FOR' => fresh_ip
|
'HTTP_X_FORWARDED_FOR' => fresh_ip
|
||||||
assert_equal 302, last_response.status
|
assert_equal 302, last_response.status
|
||||||
|
|
@ -86,7 +86,7 @@ class AppTest < Minitest::Test
|
||||||
|
|
||||||
def test_contact_rate_limited_after_five
|
def test_contact_rate_limited_after_five
|
||||||
ip = fresh_ip
|
ip = fresh_ip
|
||||||
token = csrf_token_from('/')
|
token = csrf_token_from('/contact')
|
||||||
5.times do
|
5.times do
|
||||||
post '/contact', contact_params(authenticity_token: token), 'HTTP_X_FORWARDED_FOR' => ip
|
post '/contact', contact_params(authenticity_token: token), 'HTTP_X_FORWARDED_FOR' => ip
|
||||||
assert_equal 302, last_response.status
|
assert_equal 302, last_response.status
|
||||||
|
|
|
||||||
27
views/contact.erb
Normal file
27
views/contact.erb
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
<%== erb :'partials/_site_nav', layout: false %>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Contact</h2>
|
||||||
|
|
||||||
|
<%== erb :'partials/_contact_form', layout: false %>
|
||||||
|
|
||||||
|
<dl class="kv contact-direct">
|
||||||
|
<dt>Email</dt><dd><a href="mailto:ie@asxp.io">ie@asxp.io</a></dd>
|
||||||
|
<dt>Telegram</dt><dd><a href="https://t.me/ie_asxpi">t.me/ie_asxpi</a></dd>
|
||||||
|
<dt>Phone</dt><dd class="mono">+995 595 026 471</dd>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section legal">
|
||||||
|
<h2>For invoices and contracts</h2>
|
||||||
|
<p class="legal-intro">Work is invoiced through my Individual Entrepreneur (ინდ. მეწარმე) registered in Georgia.</p>
|
||||||
|
<dl class="kv">
|
||||||
|
<dt>Legal name</dt><dd>IE Sergei Poljanski</dd>
|
||||||
|
<dt>Latin form</dt><dd class="mono">SERGEI POLJANSKI</dd>
|
||||||
|
<dt>Georgian</dt><dd>ინდ. მეწარმე სერგეი პოლჯანსკი</dd>
|
||||||
|
<dt>Legal form</dt><dd>Individual Entrepreneur (Georgia)</dd>
|
||||||
|
<dt>Tax ID</dt><dd class="mono">304813343</dd>
|
||||||
|
<dt>Registered</dt><dd>2026-05-04</dd>
|
||||||
|
<dt>Address</dt><dd>Ilia and Nino Nakashidze St, N 1, Building N3, Apt N3, Krtsanisi, Tbilisi, Georgia</dd>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
|
@ -1,54 +1,26 @@
|
||||||
|
<%== erb :'partials/_site_nav', layout: false %>
|
||||||
|
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<img src="/logo.svg" alt="IE Sergei Poljanski" class="monogram" width="56" height="56" />
|
<img src="/logo.svg" alt="IE Sergei Poljanski" class="monogram" width="56" height="56" />
|
||||||
<h1>Sergei Poljanski</h1>
|
<h1>Sergei Poljanski</h1>
|
||||||
<p class="tagline">DevOps & infrastructure engineering — Tbilisi, Georgia.</p>
|
<p class="tagline">DevOps & infrastructure engineering.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p class="home-intro">
|
||||||
|
I build and operate infrastructure for clients worldwide.
|
||||||
|
<a href="/contact">Get in touch</a>, or verify me via <a href="/keys">keys</a>.
|
||||||
|
</p>
|
||||||
|
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<h2>Services</h2>
|
<h2>Services</h2>
|
||||||
<ul class="services">
|
<ul class="services">
|
||||||
<li>IT infrastructure management</li>
|
<li>IT infrastructure management <span class="svc-tools">Hetzner Cloud · bare metal</span></li>
|
||||||
<li>System administration</li>
|
<li>System administration <span class="svc-tools">Linux · NixOS · Debian</span></li>
|
||||||
<li>DevOps engineering</li>
|
<li>Software development <span class="svc-tools">Lua · Ruby</span></li>
|
||||||
<li>Software deployment & configuration</li>
|
<li>Networking <span class="svc-tools">Traefik · DNS · WireGuard</span></li>
|
||||||
<li>CI/CD pipeline development</li>
|
<li>CI/CD pipeline development <span class="svc-tools">GitHub Actions · Forgejo Actions</span></li>
|
||||||
<li>Container orchestration (Docker, Kubernetes)</li>
|
<li>Container orchestration <span class="svc-tools">Docker · Kubernetes</span></li>
|
||||||
<li>Infrastructure as Code (Terraform, Ansible)</li>
|
<li>Infrastructure as Code <span class="svc-tools">Terraform · Ansible</span></li>
|
||||||
<li>Monitoring & observability (Prometheus, Grafana, Loki)</li>
|
<li>Monitoring & observability <span class="svc-tools">Prometheus · Grafana · Loki</span></li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="section">
|
|
||||||
<h2>Keys</h2>
|
|
||||||
<pre id="myPGP">gpg --recv-keys 85F7AFEDAB7D97BE667F99F24F8851660FA4121B</pre>
|
|
||||||
<pre id="mySSH">ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMZ+/5F7SxdTVW47iiStLpGK77oWfR5NgaK4tTSR/aVB ie+2026@asxp.io</pre>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="section">
|
|
||||||
<h2>Contact</h2>
|
|
||||||
|
|
||||||
<%== erb :'partials/_contact_form', layout: false %>
|
|
||||||
|
|
||||||
<dl class="kv contact-direct">
|
|
||||||
<dt>Email</dt><dd><a href="mailto:ie@asxp.io">ie@asxp.io</a></dd>
|
|
||||||
<dt>Telegram</dt><dd><a href="https://t.me/ie_asxpi">t.me/ie_asxpi</a></dd>
|
|
||||||
<dt>Phone</dt><dd class="mono">+995 595 026 471</dd>
|
|
||||||
</dl>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="section legal">
|
|
||||||
<h2>For invoices and contracts</h2>
|
|
||||||
<p class="legal-intro">Work is invoiced through my Individual Entrepreneur (ინდ. მეწარმე) registered in Georgia.</p>
|
|
||||||
<dl class="kv">
|
|
||||||
<dt>Legal name</dt><dd>IE Sergei Poljanski</dd>
|
|
||||||
<dt>Latin form</dt><dd class="mono">SERGEI POLJANSKI</dd>
|
|
||||||
<dt>Georgian</dt><dd>ინდ. მეწარმე სერგეი პოლჯანსკი</dd>
|
|
||||||
<dt>Legal form</dt><dd>Individual Entrepreneur (Georgia)</dd>
|
|
||||||
<dt>Tax ID</dt><dd class="mono">304813343</dd>
|
|
||||||
<dt>Registered</dt><dd>2026-05-04</dd>
|
|
||||||
<dt>Address</dt><dd>Ilia and Nino Nakashidze St, N 1, Building N3, Apt N3, Krtsanisi, Tbilisi, Georgia</dd>
|
|
||||||
</dl>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<script src="/copyPGP.js"></script>
|
|
||||||
<script src="/copySSH.js"></script>
|
|
||||||
|
|
|
||||||
21
views/keys.erb
Normal file
21
views/keys.erb
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<%== erb :'partials/_site_nav', layout: false %>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<div class="key-block">
|
||||||
|
<div class="key-head">
|
||||||
|
<span class="key-label">PGP</span>
|
||||||
|
<a class="key-file" href="/pgp.pub" download>pgp.pub ↓</a>
|
||||||
|
</div>
|
||||||
|
<pre data-copy="85F7AFEDAB7D97BE667F99F24F8851660FA4121B">85F7 AFED AB7D 97BE 667F 99F2 4F88 5166 0FA4 121B</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="key-block">
|
||||||
|
<div class="key-head">
|
||||||
|
<span class="key-label">SSH</span>
|
||||||
|
<a class="key-file" href="/id_ed25519.pub" download>id_ed25519.pub ↓</a>
|
||||||
|
</div>
|
||||||
|
<pre data-copy="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMZ+/5F7SxdTVW47iiStLpGK77oWfR5NgaK4tTSR/aVB ie+2026@asxp.io">ssh-ed25519 AAAA…R/aVB ie+2026@asxp.io</pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script src="/copy.js"></script>
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
<div class="form-error form-error-base"><%= errors[:base] %></div>
|
<div class="form-error form-error-base"><%= errors[:base] %></div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="cf-name">Name</label>
|
<label for="cf-name">Name</label>
|
||||||
<input id="cf-name" type="text" name="name" maxlength="100" required value="<%= values[:name] %>" />
|
<input id="cf-name" type="text" name="name" maxlength="100" required value="<%= values[:name] %>" />
|
||||||
|
|
@ -23,6 +24,7 @@
|
||||||
<input id="cf-email" type="email" name="email" maxlength="200" required value="<%= values[:email] %>" />
|
<input id="cf-email" type="email" name="email" maxlength="200" required value="<%= values[:email] %>" />
|
||||||
<% if errors[:email] %><span class="form-error"><%= errors[:email] %></span><% end %>
|
<% if errors[:email] %><span class="form-error"><%= errors[:email] %></span><% end %>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="cf-subject">Subject <span class="optional">(optional)</span></label>
|
<label for="cf-subject">Subject <span class="optional">(optional)</span></label>
|
||||||
|
|
|
||||||
8
views/partials/_site_nav.erb
Normal file
8
views/partials/_site_nav.erb
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<header class="site-header">
|
||||||
|
<a class="site-title" href="/">asxp.io</a>
|
||||||
|
<nav>
|
||||||
|
<a href="https://blog.asxp.io">blog</a>
|
||||||
|
<a href="/keys" class="<%= 'active' if request.path == '/keys' %>">keys</a>
|
||||||
|
<a href="/contact" class="<%= 'active' if request.path == '/contact' %>">contact</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>Thanks — your message is on its way.</h1>
|
<h1>Thanks! Your message is on its way.</h1>
|
||||||
<span class="ka">A confirmation copy has been sent to your inbox.</span>
|
<span class="ka">A confirmation copy has been sent to your inbox.</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue