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:
parent
1b773af9d3
commit
df65ed663d
6 changed files with 355 additions and 29 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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">+ 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 & physical product sales. Open source. Public domain.</p>
|
||||
<div class="landing-hero-ctas">
|
||||
<a href="/s/new" class="mps-button mps-button-green">+ 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">🎭</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">💰</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">📨</div>
|
||||
<h3 class="type-title">Subscriptions & 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">🎨</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">📈</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">👥</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">+ Open a Shop</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% endif %}
|
||||
|
||||
</section>
|
||||
|
||||
{% else %}
|
||||
|
||||
{% if products %}
|
||||
|
|
|
|||
|
|
@ -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 — 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 — 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 -%}
|
||||
|
|
|
|||
|
|
@ -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 → 2 → 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 — "Create a Shop," "Log In," "My Account" — 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 — 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 — 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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue