From e0563d62ff1c43922b15900dbaa1628af34fb0da Mon Sep 17 00:00:00 2001 From: Deeman Date: Tue, 17 Feb 2026 14:36:26 +0100 Subject: [PATCH] polish nav, planner UX, country pills, and dev magic link - Nav: Zillow-style centered logo, solid blue Sign In button - Planner: center app at 72rem, center wizard steps/header/preview - Country pills: UK/USA labels, remove Other, show permits slider inline under country so the effect is transparent and adjustable - Reset button: inline confirm (red "Sure? Reset") instead of alert - Worker: print magic link to console when DEBUG=true for local dev Co-Authored-By: Claude Opus 4.6 --- .../src/padelnomics/static/css/input.css | 78 +++++++++++++++++++ .../src/padelnomics/static/css/planner.css | 13 ++++ .../src/padelnomics/static/js/planner.js | 38 ++++++--- .../src/padelnomics/templates/base.html | 41 +++++----- padelnomics/src/padelnomics/worker.py | 6 ++ padelnomics/tests/test_calculator.py | 2 +- 6 files changed, 147 insertions(+), 31 deletions(-) diff --git a/padelnomics/src/padelnomics/static/css/input.css b/padelnomics/src/padelnomics/static/css/input.css index eca5eee..ee6eb03 100644 --- a/padelnomics/src/padelnomics/static/css/input.css +++ b/padelnomics/src/padelnomics/static/css/input.css @@ -57,6 +57,84 @@ /* ── Component classes ── */ @layer components { + /* ── Navigation (Zillow-style: links | logo | links) ── */ + .nav-bar { + position: sticky; + top: 0; + z-index: 50; + background: rgba(255,255,255,0.85); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border-bottom: 1px solid #E2E8F0; + } + .nav-inner { + max-width: 72rem; + margin: 0 auto; + padding: 0 1rem; + display: flex; + align-items: center; + justify-content: space-between; + height: 56px; + } + .nav-logo { + flex-shrink: 0; + line-height: 0; + } + .nav-logo img { + height: 32px; + width: auto; + } + .nav-links { + display: flex; + align-items: center; + gap: 1.25rem; + font-size: 0.875rem; + font-weight: 500; + } + .nav-links a { + color: #475569; + text-decoration: none; + transition: color 0.15s; + } + .nav-links a:hover { + color: #1D4ED8; + } + .nav-links--left { flex: 1; justify-content: flex-start; } + .nav-links--right { flex: 1; justify-content: flex-end; } + a.nav-auth-btn, + button.nav-auth-btn { + display: inline-flex; + align-items: center; + padding: 6px 16px; + border: none; + border-radius: 10px; + font-size: 0.8125rem; + font-weight: 600; + color: #fff; + background: #1D4ED8; + cursor: pointer; + text-decoration: none; + box-shadow: 0 2px 8px rgba(29,78,216,0.25); + transition: background 0.15s; + } + a.nav-auth-btn:hover, + button.nav-auth-btn:hover { + background: #1E40AF; + color: #fff; + } + .nav-badge { + @apply bg-electric/10 text-electric px-2 py-0.5 text-xs font-semibold rounded-full; + } + .nav-form { + margin: 0; + padding: 0; + display: inline; + } + @media (max-width: 768px) { + .nav-links { display: none; } + .nav-inner { justify-content: center; } + } + /* Page container */ .container-page { @apply max-w-6xl mx-auto px-4 sm:px-6 lg:px-8; diff --git a/padelnomics/src/padelnomics/static/css/planner.css b/padelnomics/src/padelnomics/static/css/planner.css index 3527bad..ed3fddc 100644 --- a/padelnomics/src/padelnomics/static/css/planner.css +++ b/padelnomics/src/padelnomics/static/css/planner.css @@ -32,6 +32,8 @@ color: var(--txt); background: var(--bg); min-height: 100vh; + max-width: 72rem; + margin: 0 auto; } /* Scrollbar */ @@ -349,6 +351,7 @@ transition: all 0.15s; } .btn-reset:hover { color: var(--head); border-color: var(--border-2); } +.btn-reset--confirm { color: #DC2626; border-color: #FCA5A5; background: #FEF2F2; } /* ── Pill Select ── */ .pill-group { @@ -715,6 +718,9 @@ justify-content: space-between; margin-bottom: 1.25rem; gap: 1rem; + max-width: 560px; + margin-left: auto; + margin-right: auto; } .wizard-dots { display: flex; @@ -775,6 +781,8 @@ .wizard-step { display: none; max-width: 560px; + margin-left: auto; + margin-right: auto; } .wizard-step.active { display: block; @@ -802,6 +810,9 @@ box-shadow: 0 1px 3px rgba(0,0,0,0.04); margin-top: 1.5rem; max-width: 560px; + margin-left: auto; + margin-right: auto; + max-width: 560px; } .wiz-preview__item { flex: 1; @@ -830,6 +841,8 @@ justify-content: space-between; margin-top: 12px; max-width: 560px; + margin-left: auto; + margin-right: auto; gap: 0.75rem; } .wiz-btn--back { diff --git a/padelnomics/src/padelnomics/static/js/planner.js b/padelnomics/src/padelnomics/static/js/planner.js index fd892af..13072cb 100644 --- a/padelnomics/src/padelnomics/static/js/planner.js +++ b/padelnomics/src/padelnomics/static/js/planner.js @@ -86,7 +86,7 @@ const COUNTRY_PRESETS = { NL: { permitsCompliance: 10000 }, SE: { permitsCompliance: 8000 }, UK: { permitsCompliance: 10000 }, - OTHER: { permitsCompliance: 12000 }, + US: { permitsCompliance: 15000 }, }; // Track which keys the user has manually adjusted const _userAdjusted = new Set(); @@ -102,11 +102,11 @@ function bindPills(){ }); // Apply country presets when country changes if(k==='country'){ - const preset = COUNTRY_PRESETS[v] || COUNTRY_PRESETS.OTHER; + const preset = COUNTRY_PRESETS[v] || COUNTRY_PRESETS.DE; for(const [pk,pv] of Object.entries(preset)){ if(!_userAdjusted.has(pk)){ S[pk]=pv; } } - rebuildCapexInputs(); bindSliders(); bindPills(); + buildCountryPill(); rebuildCapexInputs(); bindSliders(); bindPills(); } // Rebuild inputs if lighting options depend on venue if(k==='glassType'||k==='lightingType'){ @@ -257,8 +257,9 @@ function buildCountryPill(){ $('#inp-country').innerHTML = pillSelect('country','Country',[ {v:'DE',l:'Germany'},{v:'ES',l:'Spain'},{v:'IT',l:'Italy'}, {v:'FR',l:'France'},{v:'NL',l:'Netherlands'},{v:'SE',l:'Sweden'}, - {v:'UK',l:'United Kingdom'},{v:'OTHER',l:'Other'}, - ],'Affects regulatory cost defaults'); + {v:'UK',l:'UK'},{v:'US',l:'USA'}, + ]) + slider('permitsCompliance','Permits & Compliance',0,50000,1000,fE, + 'Building permits, noise studies, change-of-use, fire safety, and regulatory compliance. Adjusts automatically when you pick a country — feel free to override.'); } function rebuildCapexInputs(){ @@ -286,14 +287,12 @@ function rebuildCapexInputs(){ h+=slider('floorPrep','Floor Preparation',0,100000,1000,fE,'Floor leveling, sealing, and preparation for court installation in an existing rented building.')+ slider('hvacUpgrade','HVAC Upgrade',0,200000,1000,fE,'Upgrading existing HVAC in a rented building to handle sports venue airflow and humidity requirements.')+ slider('lightingUpgrade','Lighting Upgrade',0,100000,1000,fE,'Upgrading existing lighting to meet padel requirements: minimum 500 lux, no glare, even distribution across courts.')+ - slider('fitout','Fit-Out & Reception',0,300000,1000,fE,'Interior fit-out for reception, lounge, viewing areas, and common spaces when renting an existing building.')+ - slider('permitsCompliance','Permits & Compliance',0,50000,1000,fE,'Building permits, change-of-use application, fire safety review, noise assessment'); + slider('fitout','Fit-Out & Reception',0,300000,1000,fE,'Interior fit-out for reception, lounge, viewing areas, and common spaces when renting an existing building.'); } else if(!isIn){ h+=slider('outdoorFoundation','Concrete (\u20AC/m\u00B2)',0,150,1,fE,'Concrete pad per m\u00B2 for outdoor courts. Needs proper drainage, level surface, and frost-resistant construction.')+ slider('outdoorSiteWork','Site Work',0,60000,500,fE,'Grading, drainage installation, utilities connection, and site preparation for outdoor courts.')+ slider('outdoorLighting','Lighting per Court',0,20000,500,fE,'Floodlight installation per court. LED recommended for energy efficiency. Must meet competition standards if applicable.')+ - slider('outdoorFencing','Fencing',0,40000,500,fE,'Perimeter fencing around outdoor court area. Includes wind screens, security gates, and ball containment nets.')+ - slider('permitsCompliance','Permits & Compliance',0,50000,1000,fE,'Building permits, noise assessment, environmental compliance'); + slider('outdoorFencing','Fencing',0,40000,500,fE,'Perimeter fencing around outdoor court area. Includes wind screens, security gates, and ball containment nets.'); if(isBuy) h+=slider('landPriceSqm','Land Price (\u20AC/m\u00B2)',0,500,5,fE,'Land purchase price per m\u00B2. Varies by location, zoning, and accessibility.'); } h+=slider('workingCapital','Working Capital',0,200000,1000,fE,'Cash reserve for operating losses during ramp-up phase and seasonal dips. Critical buffer \u2014 underfunding is a common startup failure.')+ @@ -701,8 +700,25 @@ function loadScenario(id){ }); } +let _resetPending = false; +let _resetTimer = null; function resetToDefaults(){ - if(!confirm('Reset all assumptions to defaults?')) return; + const btn = document.getElementById('resetDefaultsBtn'); + if(!_resetPending){ + _resetPending = true; + btn.textContent = 'Sure? Reset'; + btn.classList.add('btn-reset--confirm'); + _resetTimer = setTimeout(()=>{ + _resetPending = false; + btn.textContent = 'Reset to Defaults'; + btn.classList.remove('btn-reset--confirm'); + }, 3000); + return; + } + clearTimeout(_resetTimer); + _resetPending = false; + btn.textContent = 'Reset to Defaults'; + btn.classList.remove('btn-reset--confirm'); Object.assign(S, JSON.parse(JSON.stringify(DEFAULTS))); _userAdjusted.clear(); buildInputs(); @@ -804,7 +820,7 @@ function renderWizNav(){ const COUNTRY_NAMES = { DE:'Germany',ES:'Spain',IT:'Italy',FR:'France',NL:'Netherlands', - SE:'Sweden',UK:'United Kingdom',OTHER:'Other' + SE:'Sweden',UK:'United Kingdom',US:'United States' }; function populateWizAutoFill(){ diff --git a/padelnomics/src/padelnomics/templates/base.html b/padelnomics/src/padelnomics/templates/base.html index 01d2b10..d507fc8 100644 --- a/padelnomics/src/padelnomics/templates/base.html +++ b/padelnomics/src/padelnomics/templates/base.html @@ -19,33 +19,36 @@ {% block head %}{% endblock %} - -