feat: redesign SaaS landing page, login form, and add Class Act styleguide section

- Treat 127.0.0.1 as SaaS domain in dev (fixes missing footer/logo)
- Landing page: hero with type-display, 6 feature cards grid, bottom CTA
- Login form: elevation card, styled input, mps-button-green submit, hint block
- CSS: login-card, landing-hero, landing-features, responsive breakpoints
- Styleguide: Landing Page section with class reference, Class Act section
  documenting the passwordless unified auth flow with diagram
- Update functional test for new landing copy
This commit is contained in:
russell@unturf.com 2026-03-06 10:31:58 -05:00
parent 1b773af9d3
commit df65ed663d
6 changed files with 355 additions and 29 deletions

View file

@ -181,8 +181,8 @@ def includeme(config):
"""
root_domain = request.app.get("make_post_sell.root_domain")
# For development, always treat localhost as SaaS domain for convenience
if request.domain == "localhost":
# For development, always treat localhost/127.0.0.1 as SaaS domain for convenience
if request.domain in ("localhost", "127.0.0.1"):
return True
# Standard SaaS domain check

View file

@ -299,6 +299,164 @@ section.join-form {
text-align: center;
}
/* ============================================
LOGIN CARD
============================================ */
.login-card {
max-width: 440px;
margin: var(--space-12) auto;
padding: var(--space-10) var(--space-8);
text-align: center;
background-color: var(--surface-base);
}
.login-card-title {
margin-bottom: var(--space-2);
}
.login-card-subtitle {
margin-bottom: var(--space-8);
color: var(--text-secondary);
}
.login-form {
display: grid;
gap: var(--space-4);
text-align: left;
}
.login-form-label {
margin-bottom: calc(-1 * var(--space-2));
}
.login-form-input {
width: 100%;
padding: var(--space-3) var(--space-4);
font-size: var(--text-md);
border: 2px solid var(--border-default);
border-radius: var(--radius-md);
background-color: var(--input-bg);
color: var(--input-text);
transition: border-color var(--duration-fast) var(--ease-standard);
}
.login-form-input:focus {
border-color: var(--border-focus);
outline: none;
}
.login-form-submit {
width: 100%;
font-size: var(--text-base);
padding: var(--space-3) var(--space-6);
cursor: pointer;
}
.login-card-hint {
margin-top: var(--space-6);
padding: var(--space-4);
background-color: var(--surface-dim);
border-radius: var(--radius-md);
text-align: left;
}
/* ============================================
LANDING PAGE (SaaS homepage, not logged in)
============================================ */
.landing-hero {
max-width: var(--content-width);
margin: var(--space-16) auto var(--space-12);
text-align: center;
padding: 0 var(--space-6);
}
.landing-hero-sub {
max-width: 540px;
margin: var(--space-4) auto var(--space-8);
color: var(--text-secondary);
}
.landing-hero-ctas {
display: grid;
grid-template-columns: auto auto;
gap: var(--space-4);
justify-content: center;
}
.landing-features {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--space-6);
max-width: var(--content-wide);
margin: 0 auto var(--space-12);
padding: 0 var(--space-6);
}
.landing-feature-card {
text-align: center;
padding: var(--space-8) var(--space-6);
}
.landing-feature-icon {
font-size: var(--text-4xl);
margin-bottom: var(--space-4);
}
.landing-feature-card h3 {
margin-bottom: var(--space-2);
}
.landing-cta-bottom {
max-width: var(--content-width);
margin: 0 auto var(--space-16);
padding: 0 var(--space-6);
}
.landing-cta-card {
text-align: center;
padding: var(--space-12) var(--space-8);
}
.landing-cta-card h2 {
margin-bottom: var(--space-3);
}
.landing-cta-card p {
margin-bottom: var(--space-6);
color: var(--text-secondary);
}
@media (max-width: 800px) {
.landing-hero {
margin: var(--space-10) auto var(--space-8);
}
.landing-hero-ctas {
grid-template-columns: 1fr;
}
.landing-hero-ctas .mps-button {
width: 100%;
}
.landing-features {
grid-template-columns: 1fr;
}
.login-card {
margin: var(--space-6) auto;
padding: var(--space-6) var(--space-5);
}
}
@media (min-width: 801px) and (max-width: 1000px) {
.landing-features {
grid-template-columns: repeat(2, 1fr);
}
}
input.mps-product-title,
input.mps-product-price,
input.mps-content-title {

View file

@ -4,10 +4,9 @@
{% if not request.shop %}
<section class="one-column well">
{% if request.user and request.user.authenticated %}
<section class="one-column well">
{% if request.user.active_shop %}
<b>{{ request.user.active_shop.name }}</b>
@ -19,20 +18,65 @@
<br />
<br />
<a href="/s/new" class="shop-new-button mps-button mps-button-small">&#65291; New Shop</a>
{% endif %}
</section>
{% else %}
<h2>Make. Post. Sell.</h2>
<p>Create your own shop and start selling digital products in minutes.</p>
<a href="/join-or-log-in" class="mps-button">Get Started</a>
<section class="landing-hero animate-fade-in-up">
<h1 class="type-display">Make it. Post it. Sell it.</h1>
<p class="type-body-lg landing-hero-sub">Commission-free digital downloads &amp; physical product sales. Open source. Public domain.</p>
<div class="landing-hero-ctas">
<a href="/s/new" class="mps-button mps-button-green">&#65291; Open a Shop</a>
<a href="/join-or-log-in" class="mps-button">Log In</a>
</div>
</section>
<section class="landing-features animate-fade-in-up stagger-2">
<div class="landing-feature-card well">
<div class="landing-feature-icon">&#127917;</div>
<h3 class="type-title">Watch Mode</h3>
<p class="type-body-sm">SPA media channel. Visitors browse your catalog without page reloads.</p>
</div>
<div class="landing-feature-card well">
<div class="landing-feature-icon">&#128176;</div>
<h3 class="type-title">Five Payment Options</h3>
<p class="type-body-sm">Stripe, PayPal, Monero, Dogecoin, and free coupons. Your money, your choice.</p>
</div>
<div class="landing-feature-card well">
<div class="landing-feature-icon">&#128232;</div>
<h3 class="type-title">Subscriptions &amp; Feeds</h3>
<p class="type-body-sm">Email updates, RSS, and Atom feeds. Keep your audience in the loop.</p>
</div>
<div class="landing-feature-card well">
<div class="landing-feature-icon">&#127912;</div>
<h3 class="type-title">Creative Tools</h3>
<p class="type-body-sm">Sandbox filters, karaoke vocal isolation, and more. Built for creators.</p>
</div>
<div class="landing-feature-card well">
<div class="landing-feature-icon">&#128200;</div>
<h3 class="type-title">Analytics Dashboard</h3>
<p class="type-body-sm">Views, sales, and traffic. Know what works without third-party trackers.</p>
</div>
<div class="landing-feature-card well">
<div class="landing-feature-icon">&#128101;</div>
<h3 class="type-title">Multi-tenant</h3>
<p class="type-body-sm">Run multiple shops from one account. Custom domains included.</p>
</div>
</section>
<section class="landing-cta-bottom animate-fade-in-up stagger-4">
<div class="well landing-cta-card">
<h2 class="type-headline-3">Ready to sell?</h2>
<p class="type-body">No credit card required. Set up your shop in minutes.</p>
<a href="/s/new" class="mps-button mps-button-green">&#65291; Open a Shop</a>
</div>
</section>
{% endif %}
</section>
{% else %}
{% if products %}

View file

@ -1,40 +1,42 @@
{% extends "base.j2" -%}
{% block content -%}
<section class="one-column">
<section class="log-in-form well">
<h2>Log in</h2>
<b>New and Existing Users</b>
<br>
<br>
<form method="post" action="/join-or-log-in" onsubmit="submit.disabled = true; return true;">
<section class="login-card well elevation-3 animate-fade-in-up">
<h2 class="type-headline-3 login-card-title">Welcome</h2>
<p class="type-body login-card-subtitle">New and existing users &mdash; one step to get in.</p>
<form method="post" action="/join-or-log-in" class="login-form" onsubmit="submit.disabled = true; return true;">
<input
name = "email2"
type = "email"
id = "email2_input"
class = "common-text-input"
value = ""
placeholder = "Your Email Address" />
<label for="email_input" class="type-label login-form-label">Email address</label>
<input
name = "email"
type = "email"
id = "email_input"
class = "common-text-input"
class = "login-form-input"
tabindex = "1"
value = ""
placeholder = "Your Email Address"
placeholder = "you@example.com"
required
autofocus />
<br/>
<br/>
<button type="submit" name="submit" id="submit" class="mps-button mps-button-green login-form-submit">
Send verification code
</button>
</form>
<div class="login-card-hint">
<span class="type-body-sm"><strong>What happens next?</strong> We email you a one-time code. Click it to log in &mdash; no password needed.</span>
</div>
<input type="submit" name="submit" id="submit" value="send verification code to email" />
</form>
<br/>
<p class="whats-next"><b>What's next?</b> check your email to log in!</p>
</section>
</section>
{%- endblock -%}

View file

@ -198,6 +198,8 @@
<a href="#layout">Layout</a>
<a href="#theme">Theme System</a>
<a href="#footer">Footer</a>
<a href="#landing">Landing Page</a>
<a href="#class-act">Class Act</a>
</div>
<hr/>
@ -955,6 +957,125 @@ SaaS domain: full footer with Get Started, Resources, Account
Shop domain: shop-specific footer with feeds + account</div>
</div>
<!-- ======== LANDING PAGE ======== -->
<div class="sg-section" id="landing">
<div class="sg-section-title">Landing Page</div>
<p>The SaaS homepage for unauthenticated visitors. Uses design tokens throughout. Grid-based feature cards. Responsive: 3 columns &rarr; 2 &rarr; 1.</p>
<div class="sg-code">.landing-hero — centered hero, max-width: content-width, generous vertical margin
.landing-hero-sub — subtitle, constrained width, secondary text color
.landing-hero-ctas — inline grid of CTA buttons, stacks on mobile
.landing-features — 3-col grid of feature cards, responsive breakpoints
.landing-feature-card — well with icon + title + description, centered
.landing-feature-icon — large emoji, text-4xl
.landing-cta-bottom — bottom call-to-action well, centered</div>
<div class="sg-subsection">
<div class="sg-label">Login Card</div>
<p>The unified login/signup form. Centered card with elevation, styled input, and hint block.</p>
<div class="sg-code">.login-card — max-width 440px, centered, elevation-3, surface-base bg
.login-card-title — type-headline-3, centered
.login-card-subtitle — type-body, secondary color
.login-form — grid layout, gap-4
.login-form-label — type-label, uppercase
.login-form-input — full-width, md radius, focus border animation
.login-form-submit — full-width mps-button-green
.login-card-hint — surface-dim background, explains what happens next</div>
</div>
</div>
<!-- ======== CLASS ACT ======== -->
<div class="sg-section" id="class-act">
<div class="sg-section-title">Class Act</div>
<p>Patterns that are thoughtfully designed and worth preserving. If something is a <em>class act</em>, it earned its place here through deliberate UX decisions, not accident.</p>
<div class="sg-subsection">
<div class="sg-label">Passwordless Unified Auth Flow</div>
<p>The sign-up and login flow is the same flow. There is no separate registration page. Every entry point &mdash; "Create a Shop," "Log In," "My Account" &mdash; routes to <code>/join-or-log-in</code>, which asks for one thing: your email.</p>
<div class="sg-demo">
<div style="max-width: 400px; margin: 0 auto;">
<div class="well elevation-2" style="padding: 32px 24px; text-align: center;">
<h3 class="type-headline-3" style="margin-bottom: 4px;">Welcome</h3>
<p class="type-body" style="color: var(--text-secondary); margin-bottom: 24px;">New and existing users &mdash; one step to get in.</p>
<div style="text-align: left;">
<div class="type-label" style="margin-bottom: 8px;">Email address</div>
<input type="email" class="login-form-input" placeholder="you@example.com" disabled style="margin-bottom: 16px;" />
<div class="mps-button mps-button-green" style="width: 100%; text-align: center; font-size: 16px; padding: 12px;">Send verification code</div>
</div>
<div style="margin-top: 20px; padding: 12px; background: var(--surface-dim); border-radius: 8px; text-align: left;">
<span class="type-body-sm"><strong>What happens next?</strong> We email you a one-time code. Click it to log in &mdash; no password needed.</span>
</div>
</div>
</div>
</div>
<div class="sg-subsection" style="margin-top: 24px;">
<div class="sg-label">Why this is a class act</div>
<div class="sg-code">DESIGN DECISIONS:
1. One flow for new + existing users
No "Sign up" vs "Log in" split. The system figures it out.
New email? Account created. Known email? Welcome back.
Zero cognitive load for the visitor.
2. Email-first, passwordless
No passwords to forget, leak, or brute-force.
A one-time verification code is emailed to the user.
Click the link or enter the code — done.
3. Every CTA leads here
"Create a Shop," "My Account," "Log In," "Get Started"
— they ALL route to /join-or-log-in.
One URL. One form. No dead ends.
4. Honeypot spam protection
A hidden email2 field catches bots.
No CAPTCHA friction for real humans.
FLOW DIAGRAM:
Visitor clicks any CTA
|
v
/join-or-log-in
[Enter email address]
|
v
Server checks email
|
+----+----+
| |
New user Existing user
| |
Create Look up
account account
| |
+----+----+
|
v
Email one-time code
|
v
User clicks link in email
|
v
Authenticated session
(redirect to ?next= or /)
TEMPLATE: join-or-log-in.j2
VIEW: views/user.py (join_or_log_in)
CSS: .login-card, .login-form-*</div>
</div>
</div>
</div>
<br/>
</section>

View file

@ -102,12 +102,13 @@ class UnauthenticatedFunctionalTests(FunctionalTests):
self.assertIn(b"Cart $0.00 (0)", res.body)
def test_root_home_page_landing_content(self):
"""Anonymous visitors see the MPS landing with tagline and signup CTA."""
"""Anonymous visitors see the MPS landing with hero, features, and CTAs."""
res = self.testapp.get("/", status=200)
body = res.body.decode()
self.assertIn("Make. Post. Sell.", body)
self.assertIn("start selling digital products", body)
self.assertIn("Make it. Post it. Sell it.", body)
self.assertIn("Commission-free", body)
self.assertIn("/join-or-log-in", body)
self.assertIn("/s/new", body)
def test_new_product_redirects(self):
redirect_res = self.testapp.get("/p/new", status=302)