diff --git a/make_post_sell/request_methods.py b/make_post_sell/request_methods.py index c80c96e..f75293b 100644 --- a/make_post_sell/request_methods.py +++ b/make_post_sell/request_methods.py @@ -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 diff --git a/make_post_sell/static/css/common.css b/make_post_sell/static/css/common.css index 3b1bfc2..26be9b5 100644 --- a/make_post_sell/static/css/common.css +++ b/make_post_sell/static/css/common.css @@ -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 { diff --git a/make_post_sell/templates/home.j2 b/make_post_sell/templates/home.j2 index bd77159..ce116c0 100644 --- a/make_post_sell/templates/home.j2 +++ b/make_post_sell/templates/home.j2 @@ -4,10 +4,9 @@ {% if not request.shop %} -
- {% if request.user and request.user.authenticated %} +
{% if request.user.active_shop %} {{ request.user.active_shop.name }} @@ -19,20 +18,65 @@

- + + New Shop {% endif %} +
{% else %} -

Make. Post. Sell.

-

Create your own shop and start selling digital products in minutes.

- Get Started +
+

Make it. Post it. Sell it.

+

Commission-free digital downloads & physical product sales. Open source. Public domain.

+ +
+ +
+
+
🎭
+

Watch Mode

+

SPA media channel. Visitors browse your catalog without page reloads.

+
+
+
💰
+

Five Payment Options

+

Stripe, PayPal, Monero, Dogecoin, and free coupons. Your money, your choice.

+
+
+
📨
+

Subscriptions & Feeds

+

Email updates, RSS, and Atom feeds. Keep your audience in the loop.

+
+
+
🎨
+

Creative Tools

+

Sandbox filters, karaoke vocal isolation, and more. Built for creators.

+
+
+
📈
+

Analytics Dashboard

+

Views, sales, and traffic. Know what works without third-party trackers.

+
+
+
👥
+

Multi-tenant

+

Run multiple shops from one account. Custom domains included.

+
+
+ +
+
+

Ready to sell?

+

No credit card required. Set up your shop in minutes.

+ + Open a Shop +
+
{% endif %} -
- {% else %} {% if products %} diff --git a/make_post_sell/templates/join-or-log-in.j2 b/make_post_sell/templates/join-or-log-in.j2 index 92aed0f..26693c1 100644 --- a/make_post_sell/templates/join-or-log-in.j2 +++ b/make_post_sell/templates/join-or-log-in.j2 @@ -1,40 +1,42 @@ {% extends "base.j2" -%} {% block content -%}
-
-

Log in

- New and Existing Users -
-
-
+
{%- endblock -%} diff --git a/make_post_sell/templates/styleguide.j2 b/make_post_sell/templates/styleguide.j2 index e49cadc..395a7d6 100644 --- a/make_post_sell/templates/styleguide.j2 +++ b/make_post_sell/templates/styleguide.j2 @@ -198,6 +198,8 @@ Layout Theme System Footer + Landing Page + Class Act
@@ -955,6 +957,125 @@ SaaS domain: full footer with Get Started, Resources, Account Shop domain: shop-specific footer with feeds + account + + +
+
Landing Page
+ +

The SaaS homepage for unauthenticated visitors. Uses design tokens throughout. Grid-based feature cards. Responsive: 3 columns → 2 → 1.

+ +
.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
+ +
+
Login Card
+

The unified login/signup form. Centered card with elevation, styled input, and hint block.

+
.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
+
+
+ + + +
+
Class Act
+ +

Patterns that are thoughtfully designed and worth preserving. If something is a class act, it earned its place here through deliberate UX decisions, not accident.

+ +
+
Passwordless Unified Auth Flow
+ +

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 /join-or-log-in, which asks for one thing: your email.

+ +
+
+
+

Welcome

+

New and existing users — one step to get in.

+
+
Email address
+ +
Send verification code
+
+
+ What happens next? We email you a one-time code. Click it to log in — no password needed. +
+
+
+
+ +
+
Why this is a class act
+
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-*
+
+
+ +
+
diff --git a/make_post_sell/tests/test_functional.py b/make_post_sell/tests/test_functional.py index 63105bb..3322429 100644 --- a/make_post_sell/tests/test_functional.py +++ b/make_post_sell/tests/test_functional.py @@ -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)