← back to Lawyer Directory Builder
Iter 30: /signup + /login submit-disable + loading state
370aff1116ea35d086318322ff7d01c8bd41117c · 2026-05-04 03:20:45 -0700 · SteveStudio2
Applies the iter-27 loading-state pattern to both auth forms.
/signup (was: 'Claim your listing →' static button → vanilla form post):
- 'Claiming your listing…' loading text + champagne-disc spinner
- Button disabled on submit; data-loading-text drives the swap
- Prevents double-submit during bcrypt password hash + DB INSERT
/login (was: 'Sign in →' static button):
- 'Signing in…' loading text + spinner
- Same submit-disable JS pattern
Shared CSS in portal layout:
- button.big-btn:disabled — oxidized gradient + .7 opacity (matches the
/upgrade .up-cta:disabled / /find-a-lawyer .cta-metal:disabled style)
- button.big-btn .cta-spinner — 13px champagne-bordered ring
- @keyframes portal-spin (720ms linear infinite)
- prefers-reduced-motion: animation disabled, spinner stays as static
champagne dot so the loading state still reads as a state change
Both auth surfaces now match /find-a-lawyer (consumer) and /upgrade
(payment) for submit-feedback consistency. Slow-network attorneys no
longer see a 'hung browser' on signup or login.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Files touched
Diff
commit 370aff1116ea35d086318322ff7d01c8bd41117c
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Mon May 4 03:20:45 2026 -0700
Iter 30: /signup + /login submit-disable + loading state
Applies the iter-27 loading-state pattern to both auth forms.
/signup (was: 'Claim your listing →' static button → vanilla form post):
- 'Claiming your listing…' loading text + champagne-disc spinner
- Button disabled on submit; data-loading-text drives the swap
- Prevents double-submit during bcrypt password hash + DB INSERT
/login (was: 'Sign in →' static button):
- 'Signing in…' loading text + spinner
- Same submit-disable JS pattern
Shared CSS in portal layout:
- button.big-btn:disabled — oxidized gradient + .7 opacity (matches the
/upgrade .up-cta:disabled / /find-a-lawyer .cta-metal:disabled style)
- button.big-btn .cta-spinner — 13px champagne-bordered ring
- @keyframes portal-spin (720ms linear infinite)
- prefers-reduced-motion: animation disabled, spinner stays as static
champagne dot so the loading state still reads as a state change
Both auth surfaces now match /find-a-lawyer (consumer) and /upgrade
(payment) for submit-feedback consistency. Slow-network attorneys no
longer see a 'hung browser' on signup or login.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
src/server/portal.ts | 54 +++++++++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 51 insertions(+), 3 deletions(-)
diff --git a/src/server/portal.ts b/src/server/portal.ts
index 796abba..ea6e926 100644
--- a/src/server/portal.ts
+++ b/src/server/portal.ts
@@ -138,6 +138,17 @@ function layout(title: string, body: string, opts: { user?: any } = {}) {
.cta-metal{padding:20px 36px;font-size:12px;letter-spacing:.18em;text-transform:uppercase;background:linear-gradient(180deg,#d4b683 0%,#b89968 38%,#9c7e4f 100%);color:var(--noir);border:1px solid var(--metal);font-weight:500;box-shadow:inset 0 1px 0 rgba(255,235,200,.35),0 1px 0 rgba(0,0,0,.4);transition:transform 220ms ease,box-shadow 220ms ease,background 220ms ease}
.cta-metal:hover{background:linear-gradient(180deg,#e3c89a 0%,#c8a674 50%,#a98559 100%);transform:translateY(-1px);box-shadow:inset 0 1px 0 rgba(255,240,210,.5),0 4px 18px rgba(184,153,104,.18);color:var(--noir)}
+ /* Submit-button loading state (shared by /signup and /login forms) */
+ button.big-btn:disabled{cursor:not-allowed;opacity:.7;transform:none;background:linear-gradient(180deg,#9c8758 0%,#80684a 50%,#5e4a30 100%);box-shadow:inset 0 1px 0 rgba(255,235,200,.15)}
+ button.big-btn:disabled:hover{background:linear-gradient(180deg,#9c8758 0%,#80684a 50%,#5e4a30 100%);transform:none}
+ button.big-btn .cta-spinner{display:none;width:13px;height:13px;border:1.5px solid rgba(10,10,12,.25);border-top-color:var(--noir);border-radius:50%;animation:portal-spin 720ms linear infinite;margin-left:10px;vertical-align:middle}
+ button.big-btn.is-loading .cta-arrow{display:none}
+ button.big-btn.is-loading .cta-spinner{display:inline-block}
+ @keyframes portal-spin{to{transform:rotate(360deg)}}
+ @media (prefers-reduced-motion: reduce){
+ button.big-btn .cta-spinner{animation:none}
+ }
+
*:focus-visible{outline:none;box-shadow:0 0 0 3px rgba(184,153,104,.45);border-radius:1px}
*:focus:not(:focus-visible){outline:none}
@@ -281,12 +292,31 @@ router.get('/signup', (req, res) => {
<input class="big-in" name="password" type="password" placeholder="Min 8 characters" required minlength="8" autocomplete="new-password">
</label>
- <button class="big-btn" type="submit" style="margin-top:28px">Claim your listing →</button>
+ <button class="big-btn" type="submit" style="margin-top:28px" data-loading-text="Claiming your listing…">
+ <span class="cta-text">Claim your listing</span>
+ <span class="cta-arrow" style="font-family:var(--serif);font-style:italic;margin-left:8px">→</span>
+ <span class="cta-spinner" aria-hidden="true"></span>
+ </button>
</div>
</form>
<p class="muted" style="margin-top:24px">Already claimed? <a href="/login">Sign in</a></p>
+ <script>
+ (function(){
+ var f = document.getElementById('claim-form');
+ if (!f) return;
+ f.addEventListener('submit', function(){
+ var btn = f.querySelector('button[type="submit"]');
+ if (!btn) return;
+ btn.disabled = true;
+ btn.classList.add('is-loading');
+ var t = btn.querySelector('.cta-text');
+ if (t) t.textContent = btn.dataset.loadingText || 'Submitting…';
+ });
+ })();
+ </script>
+
<style>
form.stack.big{gap:22px;max-width:720px}
.signup-vol{display:flex;align-items:center;gap:18px;margin:0 0 26px;color:var(--metal)}
@@ -527,13 +557,31 @@ router.get('/login', (req, res) => {
</div>
<h2 class="signup-display" style="font-size:clamp(36px,5vw,56px);margin:0 0 36px;max-width:14ch">Welcome <em>back.</em></h2>
${flash(req)}
- <form class="stack" method="post" action="/login" style="max-width:480px">
+ <form class="stack" method="post" action="/login" style="max-width:480px" id="login-form">
<input type="hidden" name="next" value="${esc(next)}">
<input name="email" type="email" placeholder="Email" required autocomplete="email">
<input name="password" type="password" placeholder="Password" required autocomplete="current-password">
- <button class="big-btn" type="submit" style="margin-top:8px;width:auto;align-self:flex-start">Sign in →</button>
+ <button class="big-btn" type="submit" style="margin-top:8px;width:auto;align-self:flex-start" data-loading-text="Signing in…">
+ <span class="cta-text">Sign in</span>
+ <span class="cta-arrow" style="font-family:var(--serif);font-style:italic;margin-left:8px">→</span>
+ <span class="cta-spinner" aria-hidden="true"></span>
+ </button>
</form>
<p class="muted" style="margin-top:24px">No account yet? <a href="/signup">Claim your listing</a></p>
+ <script>
+ (function(){
+ var f = document.getElementById('login-form');
+ if (!f) return;
+ f.addEventListener('submit', function(){
+ var btn = f.querySelector('button[type="submit"]');
+ if (!btn) return;
+ btn.disabled = true;
+ btn.classList.add('is-loading');
+ var t = btn.querySelector('.cta-text');
+ if (t) t.textContent = btn.dataset.loadingText || 'Signing in…';
+ });
+ })();
+ </script>
`, { user: req.user }));
});
← c9f2fae Iter 29: /methodology page — closes the Rule 7.1 substantiat
·
back to Lawyer Directory Builder
·
Iter 31: /404 — path-aware editorial recovery surface 41bda2f →