merge(i18n): i18n-foundation — complete i18n for all pages (Iterations 4–5)
Merges 11 commits covering full i18n of the padelnomics app:
- JSON locale files replacing all inline {% if lang %} blocks
- tformat Jinja2 filter for parameterized translations
- All public-facing templates: content, leads, directory, suppliers,
planner (Iteration 4)
- Auth-gated pages: dashboard, billing, supplier dashboard (all tabs),
business plan PDF (Iteration 5)
- Language detection fix for non-lang-prefixed routes (dashboard/billing)
- 1533 keys in en.json and de.json
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -54,6 +54,15 @@ def _fmt_n(n) -> str:
|
||||
return f"{round(float(n)):,}".replace(",", ".")
|
||||
|
||||
|
||||
def _tformat(s: str, **kwargs) -> str:
|
||||
"""Format a translation string with named placeholders.
|
||||
|
||||
Usage: {{ t.some_key | tformat(count=total, name=supplier.name) }}
|
||||
JSON value: "Browse {count}+ suppliers from {name}"
|
||||
"""
|
||||
return s.format_map(kwargs)
|
||||
|
||||
|
||||
def create_app() -> Quart:
|
||||
"""Create and configure the Quart application."""
|
||||
|
||||
@@ -67,12 +76,13 @@ def create_app() -> Quart:
|
||||
|
||||
app.secret_key = config.SECRET_KEY
|
||||
|
||||
# Jinja2 filters for financial formatting (used in planner templates)
|
||||
# Jinja2 filters
|
||||
app.jinja_env.filters["fmt_currency"] = _fmt_currency
|
||||
app.jinja_env.filters["fmt_k"] = _fmt_k
|
||||
app.jinja_env.filters["fmt_pct"] = _fmt_pct
|
||||
app.jinja_env.filters["fmt_x"] = _fmt_x
|
||||
app.jinja_env.filters["fmt_n"] = _fmt_n
|
||||
app.jinja_env.filters["tformat"] = _tformat # translate with placeholders: {{ t.key | tformat(count=n) }}
|
||||
|
||||
# Session config
|
||||
app.config["SESSION_COOKIE_SECURE"] = not config.DEBUG
|
||||
@@ -186,7 +196,8 @@ def create_app() -> Quart:
|
||||
@app.context_processor
|
||||
def inject_globals():
|
||||
from datetime import datetime
|
||||
lang = g.get("lang", "en")
|
||||
lang = g.get("lang") or _detect_lang()
|
||||
g.lang = lang # ensure g.lang is always set (e.g. for dashboard/billing routes)
|
||||
effective_lang = lang if lang in SUPPORTED_LANGS else "en"
|
||||
return {
|
||||
"config": config,
|
||||
|
||||
@@ -14,6 +14,7 @@ from quart import Blueprint, flash, g, jsonify, redirect, render_template, reque
|
||||
|
||||
from ..auth.routes import login_required
|
||||
from ..core import config, execute, fetch_one, get_paddle_price
|
||||
from ..i18n import get_translations
|
||||
|
||||
|
||||
def _paddle_client() -> PaddleClient:
|
||||
@@ -177,7 +178,8 @@ async def manage():
|
||||
"""Redirect to Paddle customer portal."""
|
||||
sub = await get_subscription(g.user["id"])
|
||||
if not sub or not sub.get("provider_subscription_id"):
|
||||
await flash("No active subscription found.", "error")
|
||||
t = get_translations(g.get("lang") or "en")
|
||||
await flash(t["billing_no_subscription"], "error")
|
||||
return redirect(url_for("dashboard.settings"))
|
||||
|
||||
paddle = _paddle_client()
|
||||
|
||||
@@ -1,51 +1,51 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Free Padel Court Financial Planner - {{ config.APP_NAME }}{% endblock %}
|
||||
{% block title %}{{ t.billing_pricing_title }} - {{ config.APP_NAME }}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<meta name="description" content="The most sophisticated padel court business plan calculator — completely free. 60+ variables, sensitivity analysis, cash flow projections, and supplier connections.">
|
||||
<meta property="og:title" content="Free Padel Court Financial Planner - {{ config.APP_NAME }}">
|
||||
<meta property="og:description" content="Plan your padel court investment with 60+ variables, sensitivity analysis, and professional-grade projections. No signup required. Completely free.">
|
||||
<meta name="description" content="{{ t.billing_pricing_meta_desc }}">
|
||||
<meta property="og:title" content="{{ t.billing_pricing_og_title }} - {{ config.APP_NAME }}">
|
||||
<meta property="og:description" content="{{ t.billing_pricing_og_desc }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="container-page py-12">
|
||||
<div class="heading-group text-center">
|
||||
<h1 class="text-3xl">100% Free. No Catch.</h1>
|
||||
<p>The most sophisticated padel court financial planner available — completely free. Plan your investment with 60+ variables, sensitivity analysis, and professional-grade projections.</p>
|
||||
<h1 class="text-3xl">{{ t.billing_pricing_h1 }}</h1>
|
||||
<p>{{ t.billing_pricing_subtitle }}</p>
|
||||
</div>
|
||||
|
||||
<div class="grid-2 mt-8">
|
||||
<div class="card">
|
||||
<p class="card-header">Financial Planner</p>
|
||||
<p class="text-lg font-bold text-navy mb-4">Free <span class="text-sm font-normal text-slate">— forever</span></p>
|
||||
<p class="card-header">{{ t.billing_planner_card }}</p>
|
||||
<p class="text-lg font-bold text-navy mb-4">{{ t.billing_planner_free }} <span class="text-sm font-normal text-slate">{{ t.billing_planner_forever }}</span></p>
|
||||
<ul class="space-y-2 text-sm text-slate-dark mb-6">
|
||||
<li>60+ adjustable variables</li>
|
||||
<li>6 analysis tabs (CAPEX, Operating, Cash Flow, Returns, Metrics)</li>
|
||||
<li>Sensitivity analysis (utilization + pricing)</li>
|
||||
<li>Save unlimited scenarios</li>
|
||||
<li>Interactive charts</li>
|
||||
<li>Indoor/outdoor & rent/buy models</li>
|
||||
<li>{{ t.billing_feature_1 }}</li>
|
||||
<li>{{ t.billing_feature_2 }}</li>
|
||||
<li>{{ t.billing_feature_3 }}</li>
|
||||
<li>{{ t.billing_feature_4 }}</li>
|
||||
<li>{{ t.billing_feature_5 }}</li>
|
||||
<li>{{ t.billing_feature_6 }}</li>
|
||||
</ul>
|
||||
{% if user %}
|
||||
<a href="{{ url_for('planner.index') }}" class="btn w-full text-center">Open Planner</a>
|
||||
<a href="{{ url_for('planner.index') }}" class="btn w-full text-center">{{ t.billing_open_planner }}</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('auth.signup') }}" class="btn w-full text-center">Create Free Account</a>
|
||||
<a href="{{ url_for('auth.signup') }}" class="btn w-full text-center">{{ t.billing_create_account }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<p class="card-header">Need Help Building?</p>
|
||||
<p class="text-slate-dark mb-4">We connect you with verified partners</p>
|
||||
<p class="card-header">{{ t.billing_help_card }}</p>
|
||||
<p class="text-slate-dark mb-4">{{ t.billing_help_subtitle }}</p>
|
||||
<ul class="space-y-2 text-sm text-slate-dark mb-6">
|
||||
<li>Court supplier quotes</li>
|
||||
<li>Financing & bank connections</li>
|
||||
<li>Construction planning</li>
|
||||
<li>Equipment sourcing</li>
|
||||
<li>{{ t.billing_help_feature_1 }}</li>
|
||||
<li>{{ t.billing_help_feature_2 }}</li>
|
||||
<li>{{ t.billing_help_feature_3 }}</li>
|
||||
<li>{{ t.billing_help_feature_4 }}</li>
|
||||
</ul>
|
||||
{% if user %}
|
||||
<a href="{{ url_for('leads.suppliers') }}" class="btn-outline w-full text-center">Get Supplier Quotes</a>
|
||||
<a href="{{ url_for('leads.suppliers') }}" class="btn-outline w-full text-center">{{ t.billing_get_quotes }}</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('auth.signup') }}" class="btn-outline w-full text-center">Sign Up to Get Started</a>
|
||||
<a href="{{ url_for('auth.signup') }}" class="btn-outline w-full text-center">{{ t.billing_signup }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Welcome - {{ config.APP_NAME }}{% endblock %}
|
||||
{% block title %}{{ t.billing_success_title }} - {{ config.APP_NAME }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="container-page py-12">
|
||||
<div class="card max-w-md mx-auto mt-8 text-center">
|
||||
<h1 class="text-2xl mb-4">Welcome to Padelnomics!</h1>
|
||||
<h1 class="text-2xl mb-4">{{ t.billing_success_h1 }}</h1>
|
||||
|
||||
<p class="text-slate-dark mb-6">Your account is ready. Start planning your padel court investment with our financial planner.</p>
|
||||
<p class="text-slate-dark mb-6">{{ t.billing_success_body }}</p>
|
||||
|
||||
<a href="{{ url_for('planner.index') }}" class="btn">Open Planner</a>
|
||||
<a href="{{ url_for('planner.index') }}" class="btn">{{ t.billing_success_btn }}</a>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
@@ -8,6 +8,7 @@ import json
|
||||
from pathlib import Path
|
||||
|
||||
from .core import fetch_one
|
||||
from .i18n import get_translations
|
||||
from .planner.calculator import calc, validate_state
|
||||
|
||||
TEMPLATE_DIR = Path(__file__).parent / "templates" / "businessplan"
|
||||
@@ -27,57 +28,66 @@ def _fmt_pct(n) -> str:
|
||||
return f"{n * 100:.1f}%"
|
||||
|
||||
|
||||
def _fmt_months(idx: int) -> str:
|
||||
def _fmt_months(idx: int, t: dict) -> str:
|
||||
"""Format payback month index as readable string."""
|
||||
if idx < 0:
|
||||
return "Not reached in 60 months"
|
||||
return t["bp_payback_not_reached"]
|
||||
months = idx + 1
|
||||
if months <= 12:
|
||||
return f"{months} months"
|
||||
return t["bp_months"].format(n=months)
|
||||
years = months / 12
|
||||
return f"{years:.1f} years"
|
||||
return t["bp_years"].format(n=f"{years:.1f}")
|
||||
|
||||
|
||||
def get_plan_sections(state: dict, d: dict, language: str = "en") -> dict:
|
||||
"""Extract and format all business plan sections from planner data."""
|
||||
s = state
|
||||
is_en = language == "en"
|
||||
t = get_translations(language)
|
||||
|
||||
venue_type = "Indoor" if s["venue"] == "indoor" else "Outdoor"
|
||||
own_type = "Own" if s["own"] == "buy" else "Rent"
|
||||
venue_type = t["bp_indoor"] if s["venue"] == "indoor" else t["bp_outdoor"]
|
||||
own_type = t["bp_own"] if s["own"] == "buy" else t["bp_rent"]
|
||||
|
||||
payback_str = _fmt_months(d["paybackIdx"], t)
|
||||
irr_str = _fmt_pct(d["irr"])
|
||||
total_capex_str = _fmt_eur(d["capex"])
|
||||
equity_str = _fmt_eur(d["equity"])
|
||||
loan_str = _fmt_eur(d["loanAmount"])
|
||||
per_court_str = _fmt_eur(d["capexPerCourt"])
|
||||
per_sqm_str = _fmt_eur(d["capexPerSqm"])
|
||||
|
||||
sections = {
|
||||
"title": "Padel Business Plan" if is_en else "Padel Businessplan",
|
||||
"lang": language,
|
||||
"title": t["bp_title"],
|
||||
"subtitle": f"{venue_type} ({own_type}) \u2014 {s.get('country', 'DE')}",
|
||||
"courts": f"{s['dblCourts']} double + {s['sglCourts']} single ({d['totalCourts']} total)",
|
||||
"courts": t["bp_courts_desc"].format(dbl=s["dblCourts"], sgl=s["sglCourts"], total=d["totalCourts"]),
|
||||
|
||||
# Executive Summary
|
||||
"executive_summary": {
|
||||
"heading": "Executive Summary" if is_en else "Zusammenfassung",
|
||||
"heading": t["bp_exec_summary"],
|
||||
"facility_type": f"{venue_type} ({own_type})",
|
||||
"courts": d["totalCourts"],
|
||||
"sqm": d["sqm"],
|
||||
"total_capex": _fmt_eur(d["capex"]),
|
||||
"equity": _fmt_eur(d["equity"]),
|
||||
"loan": _fmt_eur(d["loanAmount"]),
|
||||
"total_capex": total_capex_str,
|
||||
"equity": equity_str,
|
||||
"loan": loan_str,
|
||||
"y1_revenue": _fmt_eur(d["annuals"][0]["revenue"]) if d["annuals"] else "-",
|
||||
"y3_ebitda": _fmt_eur(d["stabEbitda"]),
|
||||
"irr": _fmt_pct(d["irr"]),
|
||||
"payback": _fmt_months(d["paybackIdx"]),
|
||||
"irr": irr_str,
|
||||
"payback": payback_str,
|
||||
},
|
||||
|
||||
# Investment Plan (CAPEX)
|
||||
"investment": {
|
||||
"heading": "Investment Plan" if is_en else "Investitionsplan",
|
||||
"heading": t["bp_investment"],
|
||||
"items": d["capexItems"],
|
||||
"total": _fmt_eur(d["capex"]),
|
||||
"per_court": _fmt_eur(d["capexPerCourt"]),
|
||||
"per_sqm": _fmt_eur(d["capexPerSqm"]),
|
||||
"total": total_capex_str,
|
||||
"per_court": per_court_str,
|
||||
"per_sqm": per_sqm_str,
|
||||
},
|
||||
|
||||
# Operating Costs
|
||||
"operations": {
|
||||
"heading": "Operating Costs" if is_en else "Betriebskosten",
|
||||
"heading": t["bp_operations"],
|
||||
"items": d["opexItems"],
|
||||
"monthly_total": _fmt_eur(d["opex"]),
|
||||
"annual_total": _fmt_eur(d["annualOpex"]),
|
||||
@@ -85,7 +95,7 @@ def get_plan_sections(state: dict, d: dict, language: str = "en") -> dict:
|
||||
|
||||
# Revenue Model
|
||||
"revenue": {
|
||||
"heading": "Revenue & Profitability" if is_en else "Umsatz & Rentabilit\u00e4t",
|
||||
"heading": t["bp_revenue"],
|
||||
"weighted_rate": _fmt_eur(d["weightedRate"]),
|
||||
"utilization": _fmt_pct(s["utilTarget"] / 100),
|
||||
"gross_monthly": _fmt_eur(d["grossRevMonth"]),
|
||||
@@ -96,7 +106,7 @@ def get_plan_sections(state: dict, d: dict, language: str = "en") -> dict:
|
||||
|
||||
# 5-Year P&L
|
||||
"annuals": {
|
||||
"heading": "5-Year Projection" if is_en else "5-Jahres-Projektion",
|
||||
"heading": t["bp_annuals"],
|
||||
"years": [
|
||||
{
|
||||
"year": a["year"],
|
||||
@@ -111,12 +121,12 @@ def get_plan_sections(state: dict, d: dict, language: str = "en") -> dict:
|
||||
|
||||
# Financing
|
||||
"financing": {
|
||||
"heading": "Financing Structure" if is_en else "Finanzierungsstruktur",
|
||||
"heading": t["bp_financing"],
|
||||
"loan_pct": _fmt_pct(s["loanPct"] / 100),
|
||||
"equity": _fmt_eur(d["equity"]),
|
||||
"loan": _fmt_eur(d["loanAmount"]),
|
||||
"equity": equity_str,
|
||||
"loan": loan_str,
|
||||
"interest_rate": f"{s['interestRate']}%",
|
||||
"term": f"{s['loanTerm']} years",
|
||||
"term": t["bp_years"].format(n=s["loanTerm"]),
|
||||
"monthly_payment": _fmt_eur(d["monthlyPayment"]),
|
||||
"annual_debt_service": _fmt_eur(d["annualDebtService"]),
|
||||
"ltv": _fmt_pct(d["ltv"]),
|
||||
@@ -124,11 +134,11 @@ def get_plan_sections(state: dict, d: dict, language: str = "en") -> dict:
|
||||
|
||||
# Key Metrics
|
||||
"metrics": {
|
||||
"heading": "Key Metrics" if is_en else "Kennzahlen",
|
||||
"irr": _fmt_pct(d["irr"]),
|
||||
"heading": t["bp_metrics"],
|
||||
"irr": irr_str,
|
||||
"moic": f"{d['moic']:.2f}x",
|
||||
"cash_on_cash": _fmt_pct(d["cashOnCash"]),
|
||||
"payback": _fmt_months(d["paybackIdx"]),
|
||||
"payback": payback_str,
|
||||
"break_even_util": _fmt_pct(d["breakEvenUtil"]),
|
||||
"ebitda_margin": _fmt_pct(d["ebitdaMargin"]),
|
||||
"dscr_y3": f"{d['dscr'][2]['dscr']:.2f}x" if len(d["dscr"]) >= 3 else "-",
|
||||
@@ -137,7 +147,7 @@ def get_plan_sections(state: dict, d: dict, language: str = "en") -> dict:
|
||||
|
||||
# 12-Month Cash Flow
|
||||
"cashflow_12m": {
|
||||
"heading": "12-Month Cash Flow" if is_en else "12-Monats-Liquidit\u00e4tsplan",
|
||||
"heading": t["bp_cashflow_12m"],
|
||||
"months": [
|
||||
{
|
||||
"month": m["m"],
|
||||
@@ -151,6 +161,66 @@ def get_plan_sections(state: dict, d: dict, language: str = "en") -> dict:
|
||||
for m in d["months"][:12]
|
||||
],
|
||||
},
|
||||
|
||||
# Template labels
|
||||
"labels": {
|
||||
"scenario": t["bp_lbl_scenario"],
|
||||
"generated_by": t["bp_lbl_generated_by"],
|
||||
"exec_paragraph": t["bp_exec_paragraph"].format(
|
||||
facility_type=f"{venue_type} ({own_type})",
|
||||
courts=d["totalCourts"],
|
||||
sqm=d["sqm"],
|
||||
total_capex=total_capex_str,
|
||||
equity=equity_str,
|
||||
loan=loan_str,
|
||||
irr=irr_str,
|
||||
payback=payback_str,
|
||||
),
|
||||
"total_investment": t["bp_lbl_total_investment"],
|
||||
"equity_required": t["bp_lbl_equity_required"],
|
||||
"year3_ebitda": t["bp_lbl_year3_ebitda"],
|
||||
"irr": t["bp_lbl_irr"],
|
||||
"payback_period": t["bp_lbl_payback_period"],
|
||||
"year1_revenue": t["bp_lbl_year1_revenue"],
|
||||
"item": t["bp_lbl_item"],
|
||||
"amount": t["bp_lbl_amount"],
|
||||
"notes": t["bp_lbl_notes"],
|
||||
"total_capex": t["bp_lbl_total_capex"],
|
||||
"capex_stats": t["bp_lbl_capex_stats"].format(per_court=per_court_str, per_sqm=per_sqm_str),
|
||||
"equity": t["bp_lbl_equity"],
|
||||
"loan": t["bp_lbl_loan"],
|
||||
"interest_rate": t["bp_lbl_interest_rate"],
|
||||
"loan_term": t["bp_lbl_loan_term"],
|
||||
"monthly_payment": t["bp_lbl_monthly_payment"],
|
||||
"annual_debt_service": t["bp_lbl_annual_debt_service"],
|
||||
"ltv": t["bp_lbl_ltv"],
|
||||
"monthly": t["bp_lbl_monthly"],
|
||||
"total_monthly_opex": t["bp_lbl_total_monthly_opex"],
|
||||
"annual_opex": t["bp_lbl_annual_opex"],
|
||||
"weighted_hourly_rate": t["bp_lbl_weighted_hourly_rate"],
|
||||
"target_utilization": t["bp_lbl_target_utilization"],
|
||||
"gross_monthly_revenue": t["bp_lbl_gross_monthly_revenue"],
|
||||
"net_monthly_revenue": t["bp_lbl_net_monthly_revenue"],
|
||||
"monthly_ebitda": t["bp_lbl_monthly_ebitda"],
|
||||
"monthly_net_cf": t["bp_lbl_monthly_net_cf"],
|
||||
"year": t["bp_lbl_year"],
|
||||
"revenue": t["bp_lbl_revenue"],
|
||||
"ebitda": t["bp_lbl_ebitda"],
|
||||
"debt_service": t["bp_lbl_debt_service"],
|
||||
"net_cf": t["bp_lbl_net_cf"],
|
||||
"moic": t["bp_lbl_moic"],
|
||||
"cash_on_cash": t["bp_lbl_cash_on_cash"],
|
||||
"payback": t["bp_lbl_payback"],
|
||||
"break_even_util": t["bp_lbl_break_even_util"],
|
||||
"ebitda_margin": t["bp_lbl_ebitda_margin"],
|
||||
"dscr_y3": t["bp_lbl_dscr_y3"],
|
||||
"yield_on_cost": t["bp_lbl_yield_on_cost"],
|
||||
"month": t["bp_lbl_month"],
|
||||
"opex": t["bp_lbl_opex"],
|
||||
"debt": t["bp_lbl_debt"],
|
||||
"cumulative": t["bp_lbl_cumulative"],
|
||||
"disclaimer": t["bp_lbl_disclaimer"],
|
||||
},
|
||||
}
|
||||
|
||||
return sections
|
||||
|
||||
@@ -10,6 +10,7 @@ from markupsafe import Markup
|
||||
from quart import Blueprint, abort, g, render_template, request
|
||||
|
||||
from ..core import capture_waitlist_email, config, csrf_protect, fetch_all, fetch_one, waitlist_gate
|
||||
from ..i18n import get_translations
|
||||
|
||||
bp = Blueprint(
|
||||
"content",
|
||||
@@ -37,9 +38,20 @@ SECTION_TEMPLATES = {
|
||||
}
|
||||
|
||||
# Standalone Jinja2 env for baking scenario cards into static HTML.
|
||||
# Does not require a Quart app context.
|
||||
# Does not use a Quart request context, so url_for and t are injected
|
||||
# explicitly. Baked content is always EN (admin operation).
|
||||
_TEMPLATE_DIR = Path(__file__).parent / "templates"
|
||||
_bake_env = Environment(loader=FileSystemLoader(str(_TEMPLATE_DIR)), autoescape=True)
|
||||
_bake_env.filters["tformat"] = lambda s, **kw: s.format_map(kw)
|
||||
|
||||
# Hardcoded EN URL stubs — the bake env has no request context so Quart's
|
||||
# url_for cannot be used. Only endpoints referenced by scenario card templates
|
||||
# need to be listed here.
|
||||
_BAKE_URLS: dict[str, str] = {
|
||||
"planner.index": "/en/planner/",
|
||||
"directory.index": "/en/directory/",
|
||||
}
|
||||
_bake_env.globals["url_for"] = lambda endpoint, **kw: _BAKE_URLS.get(endpoint, f"/{endpoint}")
|
||||
|
||||
|
||||
def is_reserved_path(url_path: str) -> bool:
|
||||
@@ -79,7 +91,12 @@ async def bake_scenario_cards(html: str) -> str:
|
||||
state_data = json.loads(scenario["state_json"])
|
||||
|
||||
tmpl = _bake_env.get_template(template_name)
|
||||
card_html = tmpl.render(scenario=scenario, d=calc_data, s=state_data)
|
||||
# Baking is always in the EN admin context; t and lang are required
|
||||
# by scenario card templates for translated labels.
|
||||
card_html = tmpl.render(
|
||||
scenario=scenario, d=calc_data, s=state_data,
|
||||
lang="en", t=get_translations("en"),
|
||||
)
|
||||
html = html[:match.start()] + card_html + html[match.end():]
|
||||
|
||||
return html
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<header class="mb-8">
|
||||
<h1 class="text-3xl mb-2">{{ article.title }}</h1>
|
||||
<p class="text-sm text-slate">
|
||||
{% if article.published_at %}{% if lang == 'de' %}Veröffentlicht{% else %}Published{% endif %} {{ article.published_at[:10] }} · {% endif %}{% if lang == 'de' %}Padelnomics Forschung{% else %}Padelnomics Research{% endif %}
|
||||
{% if article.published_at %}{{ t.article_detail_published_label }} {{ article.published_at[:10] }} · {% endif %}{{ t.article_detail_research_label }}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{% if lang == 'de' %}Padel-Märkte - {{ config.APP_NAME }}{% else %}Padel Markets - {{ config.APP_NAME }}{% endif %}{% endblock %}
|
||||
{% block title %}{{ t.markets_page_title }} - {{ config.APP_NAME }}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<meta name="description" content="{% if lang == 'de' %}Padel-Platz-Kostenanalyse und Marktdaten für Städte weltweit. Echte Finanzszenarien mit lokalen Daten.{% else %}Padel court cost analysis and market data for cities worldwide. Real financial scenarios with local data.{% endif %}">
|
||||
<meta property="og:title" content="{% if lang == 'de' %}Padel-Märkte - {{ config.APP_NAME }}{% else %}Padel Markets - {{ config.APP_NAME }}{% endif %}">
|
||||
<meta property="og:description" content="{% if lang == 'de' %}Erkunde Padel-Platz-Kosten, Umsatzprojektionen und Investitionsrenditen nach Stadt.{% else %}Explore padel court costs, revenue projections, and investment returns by city.{% endif %}">
|
||||
<meta name="description" content="{{ t.markets_page_description }}">
|
||||
<meta property="og:title" content="{{ t.markets_page_og_title }} - {{ config.APP_NAME }}">
|
||||
<meta property="og:description" content="{{ t.markets_page_og_description }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@@ -19,7 +19,7 @@
|
||||
<div class="card mb-8">
|
||||
<div style="display: grid; grid-template-columns: 1fr auto auto; gap: 1rem; align-items: end;">
|
||||
<div>
|
||||
<label class="form-label" for="market-q">{% if lang == 'de' %}Suche{% else %}Search{% endif %}</label>
|
||||
<label class="form-label" for="market-q">{{ t.markets_search_label }}</label>
|
||||
<input type="text" id="market-q" name="q" value="{{ current_q }}" placeholder="{{ t.mkt_search_placeholder }}"
|
||||
class="form-input"
|
||||
hx-get="{{ url_for('content.market_results') }}"
|
||||
@@ -28,7 +28,7 @@
|
||||
hx-include="#market-country, #market-region">
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label" for="market-country">{% if lang == 'de' %}Land{% else %}Country{% endif %}</label>
|
||||
<label class="form-label" for="market-country">{{ t.markets_country_label }}</label>
|
||||
<select id="market-country" name="country" class="form-input"
|
||||
hx-get="{{ url_for('content.market_results') }}"
|
||||
hx-target="#market-results"
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<div class="scenario-widget scenario-capex">
|
||||
<div class="scenario-widget__header">
|
||||
<span class="scenario-widget__location">{{ scenario.location }}, {{ scenario.country }}</span>
|
||||
<span class="scenario-widget__config">{% if lang == 'de' %}Investitionsaufschlüsselung{% else %}Investment Breakdown{% endif %}</span>
|
||||
<span class="scenario-widget__config">{{ t.scenario_investment_breakdown_title }}</span>
|
||||
</div>
|
||||
<div class="scenario-widget__body">
|
||||
<div class="scenario-widget__table-wrap">
|
||||
<table class="scenario-widget__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% if lang == 'de' %}Position{% else %}Item{% endif %}</th>
|
||||
<th class="text-right">{% if lang == 'de' %}Betrag{% else %}Amount{% endif %}</th>
|
||||
<th>{{ t.scenario_table_item_label }}</th>
|
||||
<th class="text-right">{{ t.scenario_table_amount_label }}</th>
|
||||
<th>Detail</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -24,7 +24,7 @@
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td><strong>{% if lang == 'de' %}Gesamt-CAPEX{% else %}Total CAPEX{% endif %}</strong></td>
|
||||
<td><strong>{{ t.scenario_total_capex_label }}</strong></td>
|
||||
<td class="text-right mono"><strong>€{{ "{:,.0f}".format(d.capex) }}</strong></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
@@ -33,7 +33,7 @@
|
||||
</div>
|
||||
<div class="scenario-widget__metrics" style="margin-top: 1rem;">
|
||||
<div class="scenario-widget__metric">
|
||||
<span class="scenario-widget__metric-label">{% if lang == 'de' %}Pro Platz{% else %}Per Court{% endif %}</span>
|
||||
<span class="scenario-widget__metric-label">{{ t.scenario_per_court_label }}</span>
|
||||
<span class="scenario-widget__metric-value">€{{ "{:,.0f}".format(d.capexPerCourt) }}</span>
|
||||
</div>
|
||||
<div class="scenario-widget__metric">
|
||||
@@ -47,20 +47,20 @@
|
||||
</div>
|
||||
<div class="scenario-widget__metrics">
|
||||
<div class="scenario-widget__metric">
|
||||
<span class="scenario-widget__metric-label">{% if lang == 'de' %}Eigenkapital{% else %}Equity{% endif %}</span>
|
||||
<span class="scenario-widget__metric-label">{{ t.scenario_equity_label }}</span>
|
||||
<span class="scenario-widget__metric-value">€{{ "{:,.0f}".format(d.equity) }}</span>
|
||||
</div>
|
||||
<div class="scenario-widget__metric">
|
||||
<span class="scenario-widget__metric-label">{% if lang == 'de' %}Darlehen{% else %}Loan{% endif %}</span>
|
||||
<span class="scenario-widget__metric-label">{{ t.scenario_loan_label }}</span>
|
||||
<span class="scenario-widget__metric-value">€{{ "{:,.0f}".format(d.loanAmount) }}</span>
|
||||
</div>
|
||||
<div class="scenario-widget__metric">
|
||||
<span class="scenario-widget__metric-label">{% if lang == 'de' %}Gesamt{% else %}Total{% endif %}</span>
|
||||
<span class="scenario-widget__metric-label">{{ t.scenario_total_label }}</span>
|
||||
<span class="scenario-widget__metric-value">€{{ "{:,.0f}".format(d.capex) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="scenario-widget__cta">
|
||||
<a href="/planner/">{% if lang == 'de' %}Mit eigenen Zahlen testen →{% else %}Try with your own numbers →{% endif %}</a>
|
||||
<a href="{{ url_for('planner.index') }}">{{ t.scenario_cta_try_numbers }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="scenario-widget scenario-cashflow">
|
||||
<div class="scenario-widget__header">
|
||||
<span class="scenario-widget__location">{{ scenario.location }}, {{ scenario.country }}</span>
|
||||
<span class="scenario-widget__config">{% if lang == 'de' %}{{ s.holdYears }}-Jahres-Projektion{% else %}{{ s.holdYears }}-Year Projection{% endif %}</span>
|
||||
<span class="scenario-widget__config">{{ t.scenario_cashflow_config_title | tformat(years=s.holdYears) }}</span>
|
||||
</div>
|
||||
<div class="scenario-widget__body">
|
||||
<div class="scenario-widget__table-wrap">
|
||||
@@ -10,13 +10,13 @@
|
||||
<tr>
|
||||
<th></th>
|
||||
{% for a in d.annuals %}
|
||||
<th class="text-right">{% if lang == 'de' %}Jahr{% else %}Year{% endif %} {{ a.year }}</th>
|
||||
<th class="text-right">{{ t.scenario_year_label }} {{ a.year }}</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{% if lang == 'de' %}Umsatz{% else %}Revenue{% endif %}</td>
|
||||
<td>{{ t.scenario_revenue_label }}</td>
|
||||
{% for a in d.annuals %}
|
||||
<td class="text-right mono">€{{ "{:,.0f}".format(a.revenue) }}</td>
|
||||
{% endfor %}
|
||||
@@ -28,19 +28,19 @@
|
||||
{% endfor %}
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% if lang == 'de' %}Schuldendienst{% else %}Debt Service{% endif %}</td>
|
||||
<td>{{ t.scenario_debt_service_label }}</td>
|
||||
{% for a in d.annuals %}
|
||||
<td class="text-right mono">€{{ "{:,.0f}".format(a.ds) }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>{% if lang == 'de' %}Netto-Cashflow{% else %}Net Cash Flow{% endif %}</strong></td>
|
||||
<td><strong>{{ t.scenario_net_cashflow_label }}</strong></td>
|
||||
{% for a in d.annuals %}
|
||||
<td class="text-right mono"><strong>€{{ "{:,.0f}".format(a.ncf) }}</strong></td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% if lang == 'de' %}Kumulativer NCF{% else %}Cumulative NCF{% endif %}</td>
|
||||
<td>{{ t.scenario_cumulative_ncf_label }}</td>
|
||||
{% set cum = namespace(total=-d.capex) %}
|
||||
{% for a in d.annuals %}
|
||||
{% set cum.total = cum.total + a.ncf %}
|
||||
@@ -60,6 +60,6 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="scenario-widget__cta">
|
||||
<a href="/planner/">{% if lang == 'de' %}Mit eigenen Zahlen testen →{% else %}Try with your own numbers →{% endif %}</a>
|
||||
<a href="{{ url_for('planner.index') }}">{{ t.scenario_cta_try_numbers }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
<div class="scenario-widget scenario-operating">
|
||||
<div class="scenario-widget__header">
|
||||
<span class="scenario-widget__location">{{ scenario.location }}, {{ scenario.country }}</span>
|
||||
<span class="scenario-widget__config">{% if lang == 'de' %}Umsatz & Betriebskosten{% else %}Revenue & Operating Costs{% endif %}</span>
|
||||
<span class="scenario-widget__config">{{ t.scenario_revenue_opex_title }}</span>
|
||||
</div>
|
||||
<div class="scenario-widget__body">
|
||||
<h4 class="scenario-widget__section-title">{% if lang == 'de' %}Umsatzmodell{% else %}Revenue Model{% endif %}</h4>
|
||||
<h4 class="scenario-widget__section-title">{{ t.scenario_revenue_model_title }}</h4>
|
||||
<div class="scenario-widget__metrics">
|
||||
<div class="scenario-widget__metric">
|
||||
<span class="scenario-widget__metric-label">{% if lang == 'de' %}Gewichteter Satz{% else %}Weighted Rate{% endif %}</span>
|
||||
<span class="scenario-widget__metric-label">{{ t.scenario_weighted_rate_label }}</span>
|
||||
<span class="scenario-widget__metric-value">€{{ "{:.0f}".format(d.weightedRate) }}/hr</span>
|
||||
</div>
|
||||
<div class="scenario-widget__metric">
|
||||
<span class="scenario-widget__metric-label">{% if lang == 'de' %}Auslastungsziel{% else %}Utilization Target{% endif %}</span>
|
||||
<span class="scenario-widget__metric-label">{{ t.scenario_utilization_target_label }}</span>
|
||||
<span class="scenario-widget__metric-value">{{ s.utilTarget }}%</span>
|
||||
</div>
|
||||
<div class="scenario-widget__metric">
|
||||
<span class="scenario-widget__metric-label">{% if lang == 'de' %}Gebuchte Std./Monat{% else %}Booked Hours/mo{% endif %}</span>
|
||||
<span class="scenario-widget__metric-label">{{ t.scenario_booked_hours_label }}</span>
|
||||
<span class="scenario-widget__metric-value">{{ "{:,.0f}".format(d.bookedHoursMonth) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="scenario-widget__section-title">{% if lang == 'de' %}Monatliche OPEX{% else %}Monthly OPEX{% endif %}</h4>
|
||||
<h4 class="scenario-widget__section-title">{{ t.scenario_monthly_opex_title }}</h4>
|
||||
<div class="scenario-widget__table-wrap">
|
||||
<table class="scenario-widget__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% if lang == 'de' %}Position{% else %}Item{% endif %}</th>
|
||||
<th class="text-right">{% if lang == 'de' %}Monatlich{% else %}Monthly{% endif %}</th>
|
||||
<th>{{ t.scenario_table_item_label }}</th>
|
||||
<th class="text-right">{{ t.scenario_table_monthly_label }}</th>
|
||||
<th>Detail</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -41,7 +41,7 @@
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td><strong>{% if lang == 'de' %}Monatliche OPEX gesamt{% else %}Total Monthly OPEX{% endif %}</strong></td>
|
||||
<td><strong>{{ t.scenario_total_monthly_opex_label }}</strong></td>
|
||||
<td class="text-right mono"><strong>€{{ "{:,.0f}".format(d.opex) }}</strong></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
@@ -49,24 +49,24 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h4 class="scenario-widget__section-title">{% if lang == 'de' %}Monatliche Übersicht{% else %}Monthly Summary{% endif %}</h4>
|
||||
<h4 class="scenario-widget__section-title">{{ t.scenario_monthly_summary_title }}</h4>
|
||||
<div class="scenario-widget__table-wrap">
|
||||
<table class="scenario-widget__table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{% if lang == 'de' %}Bruttoumsatz{% else %}Gross Revenue{% endif %}</td>
|
||||
<td>{{ t.scenario_gross_revenue_label }}</td>
|
||||
<td class="text-right mono">€{{ "{:,.0f}".format(d.grossRevMonth) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% if lang == 'de' %}Buchungsgebühren{% else %}Booking Fees{% endif %}</td>
|
||||
<td>{{ t.scenario_booking_fees_label }}</td>
|
||||
<td class="text-right mono">-€{{ "{:,.0f}".format(d.feeDeduction) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% if lang == 'de' %}Nettoumsatz{% else %}Net Revenue{% endif %}</td>
|
||||
<td>{{ t.scenario_net_revenue_label }}</td>
|
||||
<td class="text-right mono">€{{ "{:,.0f}".format(d.netRevMonth) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% if lang == 'de' %}Betriebskosten{% else %}Operating Costs{% endif %}</td>
|
||||
<td>{{ t.scenario_operating_costs_label }}</td>
|
||||
<td class="text-right mono">-€{{ "{:,.0f}".format(d.opex) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -74,11 +74,11 @@
|
||||
<td class="text-right mono"><strong>€{{ "{:,.0f}".format(d.ebitdaMonth) }}</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% if lang == 'de' %}Schuldendienst{% else %}Debt Service{% endif %}</td>
|
||||
<td>{{ t.scenario_debt_service_label }}</td>
|
||||
<td class="text-right mono">-€{{ "{:,.0f}".format(d.monthlyPayment) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>{% if lang == 'de' %}Netto-Cashflow{% else %}Net Cash Flow{% endif %}</strong></td>
|
||||
<td><strong>{{ t.scenario_net_cashflow_label }}</strong></td>
|
||||
<td class="text-right mono"><strong>€{{ "{:,.0f}".format(d.netCFMonth) }}</strong></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -86,6 +86,6 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="scenario-widget__cta">
|
||||
<a href="/planner/">{% if lang == 'de' %}Mit eigenen Zahlen testen →{% else %}Try with your own numbers →{% endif %}</a>
|
||||
<a href="{{ url_for('planner.index') }}">{{ t.scenario_cta_try_numbers }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<div class="scenario-widget scenario-returns">
|
||||
<div class="scenario-widget__header">
|
||||
<span class="scenario-widget__location">{{ scenario.location }}, {{ scenario.country }}</span>
|
||||
<span class="scenario-widget__config">{% if lang == 'de' %}Renditen & Finanzierung{% else %}Returns & Financing{% endif %}</span>
|
||||
<span class="scenario-widget__config">{{ t.scenario_returns_config_title }}</span>
|
||||
</div>
|
||||
<div class="scenario-widget__body">
|
||||
<h4 class="scenario-widget__section-title">{% if lang == 'de' %}Renditekennzahlen{% else %}Return Metrics{% endif %}</h4>
|
||||
<h4 class="scenario-widget__section-title">{{ t.scenario_return_metrics_title }}</h4>
|
||||
<div class="scenario-widget__metrics">
|
||||
<div class="scenario-widget__metric">
|
||||
<span class="scenario-widget__metric-label">IRR ({{ s.holdYears }}yr)</span>
|
||||
@@ -15,8 +15,8 @@
|
||||
<span class="scenario-widget__metric-value">{{ "{:.2f}".format(d.moic) }}x</span>
|
||||
</div>
|
||||
<div class="scenario-widget__metric">
|
||||
<span class="scenario-widget__metric-label">{% if lang == 'de' %}Amortisation{% else %}Payback{% endif %}</span>
|
||||
<span class="scenario-widget__metric-value">{% if d.paybackIdx >= 0 %}{{ d.paybackIdx + 1 }} {% if lang == 'de' %}Monate{% else %}months{% endif %}{% else %}N/A{% endif %}</span>
|
||||
<span class="scenario-widget__metric-label">{{ t.scenario_payback_label }}</span>
|
||||
<span class="scenario-widget__metric-value">{% if d.paybackIdx >= 0 %}{{ d.paybackIdx + 1 }} {{ t.scenario_months_unit }}{% else %}N/A{% endif %}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="scenario-widget__metrics">
|
||||
@@ -25,48 +25,48 @@
|
||||
<span class="scenario-widget__metric-value">{{ "{:.1f}".format(d.cashOnCash * 100) }}%</span>
|
||||
</div>
|
||||
<div class="scenario-widget__metric">
|
||||
<span class="scenario-widget__metric-label">{% if lang == 'de' %}Rendite auf Kosten{% else %}Yield on Cost{% endif %}</span>
|
||||
<span class="scenario-widget__metric-label">{{ t.scenario_yield_on_cost_label }}</span>
|
||||
<span class="scenario-widget__metric-value">{{ "{:.1f}".format(d.yieldOnCost * 100) }}%</span>
|
||||
</div>
|
||||
<div class="scenario-widget__metric">
|
||||
<span class="scenario-widget__metric-label">{% if lang == 'de' %}EBITDA-Marge{% else %}EBITDA Margin{% endif %}</span>
|
||||
<span class="scenario-widget__metric-label">{{ t.scenario_ebitda_margin_label }}</span>
|
||||
<span class="scenario-widget__metric-value">{{ "{:.0f}".format(d.ebitdaMargin * 100) }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="scenario-widget__section-title">{% if lang == 'de' %}Exit-Analyse{% else %}Exit Analysis{% endif %}</h4>
|
||||
<h4 class="scenario-widget__section-title">{{ t.scenario_exit_analysis_title }}</h4>
|
||||
<div class="scenario-widget__metrics">
|
||||
<div class="scenario-widget__metric">
|
||||
<span class="scenario-widget__metric-label">{% if lang == 'de' %}Exit-Wert ({{ s.exitMultiple }}x EBITDA){% else %}Exit Value ({{ s.exitMultiple }}x EBITDA){% endif %}</span>
|
||||
<span class="scenario-widget__metric-label">{{ t.scenario_exit_value_label | tformat(multiple=s.exitMultiple) }}</span>
|
||||
<span class="scenario-widget__metric-value">€{{ "{:,.0f}".format(d.exitValue) }}</span>
|
||||
</div>
|
||||
<div class="scenario-widget__metric">
|
||||
<span class="scenario-widget__metric-label">{% if lang == 'de' %}Restschuld{% else %}Remaining Loan{% endif %}</span>
|
||||
<span class="scenario-widget__metric-label">{{ t.scenario_remaining_loan_label }}</span>
|
||||
<span class="scenario-widget__metric-value">€{{ "{:,.0f}".format(d.remainingLoan) }}</span>
|
||||
</div>
|
||||
<div class="scenario-widget__metric">
|
||||
<span class="scenario-widget__metric-label">{% if lang == 'de' %}Netto-Exit{% else %}Net Exit{% endif %}</span>
|
||||
<span class="scenario-widget__metric-label">{{ t.scenario_net_exit_label }}</span>
|
||||
<span class="scenario-widget__metric-value">€{{ "{:,.0f}".format(d.netExit) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="scenario-widget__section-title">{% if lang == 'de' %}Finanzierung{% else %}Financing{% endif %}</h4>
|
||||
<h4 class="scenario-widget__section-title">{{ t.scenario_financing_title }}</h4>
|
||||
<div class="scenario-widget__metrics">
|
||||
<div class="scenario-widget__metric">
|
||||
<span class="scenario-widget__metric-label">{% if lang == 'de' %}Darlehensbetrag{% else %}Loan Amount{% endif %}</span>
|
||||
<span class="scenario-widget__metric-label">{{ t.scenario_loan_amount_label }}</span>
|
||||
<span class="scenario-widget__metric-value">€{{ "{:,.0f}".format(d.loanAmount) }}</span>
|
||||
</div>
|
||||
<div class="scenario-widget__metric">
|
||||
<span class="scenario-widget__metric-label">{% if lang == 'de' %}Zinssatz / Laufzeit{% else %}Rate / Term{% endif %}</span>
|
||||
<span class="scenario-widget__metric-label">{{ t.scenario_rate_term_label }}</span>
|
||||
<span class="scenario-widget__metric-value">{{ s.interestRate }}% / {{ s.loanTerm }}yr</span>
|
||||
</div>
|
||||
<div class="scenario-widget__metric">
|
||||
<span class="scenario-widget__metric-label">{% if lang == 'de' %}Monatliche Rate{% else %}Monthly Payment{% endif %}</span>
|
||||
<span class="scenario-widget__metric-label">{{ t.scenario_monthly_payment_label }}</span>
|
||||
<span class="scenario-widget__metric-value">€{{ "{:,.0f}".format(d.monthlyPayment) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="scenario-widget__cta">
|
||||
<a href="/planner/">{% if lang == 'de' %}Mit eigenen Zahlen testen →{% else %}Try with your own numbers →{% endif %}</a>
|
||||
<a href="{{ url_for('planner.index') }}">{{ t.scenario_cta_try_numbers }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
<div class="scenario-widget__body">
|
||||
<div class="scenario-widget__metrics">
|
||||
<div class="scenario-widget__metric">
|
||||
<span class="scenario-widget__metric-label">{% if lang == 'de' %}Gesamt-CAPEX{% else %}Total CAPEX{% endif %}</span>
|
||||
<span class="scenario-widget__metric-label">{{ t.scenario_total_capex_label }}</span>
|
||||
<span class="scenario-widget__metric-value">€{{ "{:,.0f}".format(d.capex) }}</span>
|
||||
</div>
|
||||
<div class="scenario-widget__metric">
|
||||
<span class="scenario-widget__metric-label">{% if lang == 'de' %}Monatliches EBITDA{% else %}Monthly EBITDA{% endif %}</span>
|
||||
<span class="scenario-widget__metric-label">{{ t.scenario_monthly_ebitda_label }}</span>
|
||||
<span class="scenario-widget__metric-value">€{{ "{:,.0f}".format(d.ebitdaMonth) }}</span>
|
||||
</div>
|
||||
<div class="scenario-widget__metric">
|
||||
@@ -20,20 +20,20 @@
|
||||
</div>
|
||||
<div class="scenario-widget__metrics">
|
||||
<div class="scenario-widget__metric">
|
||||
<span class="scenario-widget__metric-label">{% if lang == 'de' %}Amortisation{% else %}Payback{% endif %}</span>
|
||||
<span class="scenario-widget__metric-value">{% if d.paybackIdx >= 0 %}{{ d.paybackIdx + 1 }} {% if lang == 'de' %}Monate{% else %}months{% endif %}{% else %}N/A{% endif %}</span>
|
||||
<span class="scenario-widget__metric-label">{{ t.scenario_payback_label }}</span>
|
||||
<span class="scenario-widget__metric-value">{% if d.paybackIdx >= 0 %}{{ d.paybackIdx + 1 }} {{ t.scenario_months_unit }}{% else %}N/A{% endif %}</span>
|
||||
</div>
|
||||
<div class="scenario-widget__metric">
|
||||
<span class="scenario-widget__metric-label">Cash-on-Cash</span>
|
||||
<span class="scenario-widget__metric-value">{{ "{:.1f}".format(d.cashOnCash * 100) }}%</span>
|
||||
</div>
|
||||
<div class="scenario-widget__metric">
|
||||
<span class="scenario-widget__metric-label">{% if lang == 'de' %}EBITDA-Marge{% else %}EBITDA Margin{% endif %}</span>
|
||||
<span class="scenario-widget__metric-label">{{ t.scenario_ebitda_margin_label }}</span>
|
||||
<span class="scenario-widget__metric-value">{{ "{:.0f}".format(d.ebitdaMargin * 100) }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="scenario-widget__cta">
|
||||
<a href="/planner/">{% if lang == 'de' %}Mit eigenen Zahlen testen →{% else %}Try with your own numbers →{% endif %}</a>
|
||||
<a href="{{ url_for('planner.index') }}">{{ t.scenario_cta_try_numbers }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ from quart import Blueprint, flash, g, redirect, render_template, request, url_f
|
||||
|
||||
from ..auth.routes import login_required, update_user
|
||||
from ..core import csrf_protect, fetch_one, soft_delete
|
||||
from ..i18n import get_translations
|
||||
|
||||
bp = Blueprint(
|
||||
"dashboard",
|
||||
@@ -58,7 +59,8 @@ async def settings():
|
||||
name=form.get("name", "").strip() or None,
|
||||
updated_at=datetime.utcnow().isoformat(),
|
||||
)
|
||||
await flash("Settings saved!", "success")
|
||||
t = get_translations(g.get("lang") or "en")
|
||||
await flash(t["dash_settings_saved"], "success")
|
||||
return redirect(url_for("dashboard.settings"))
|
||||
|
||||
return await render_template("settings.html")
|
||||
@@ -71,5 +73,6 @@ async def delete_account():
|
||||
from quart import session
|
||||
await soft_delete("users", g.user["id"])
|
||||
session.clear()
|
||||
await flash("Your account has been deleted.", "info")
|
||||
t = get_translations(g.lang)
|
||||
await flash(t["dash_account_deleted"], "info")
|
||||
return redirect(url_for("public.landing"))
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Dashboard - {{ config.APP_NAME }}{% endblock %}
|
||||
{% block title %}{{ t.dash_page_title }} - {{ config.APP_NAME }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="container-page py-12">
|
||||
<h1 class="text-2xl mb-1">Dashboard</h1>
|
||||
<p class="text-slate mb-8">Welcome back{% if user.name %}, {{ user.name }}{% endif %}!</p>
|
||||
<h1 class="text-2xl mb-1">{{ t.dash_h1 }}</h1>
|
||||
<p class="text-slate mb-8">{{ t.dash_welcome }}{% if user.name %}, {{ user.name }}{% endif %}!</p>
|
||||
|
||||
<div class="grid-3 mb-10">
|
||||
<div class="card text-center">
|
||||
<p class="card-header">Saved Scenarios</p>
|
||||
<p class="card-header">{{ t.dash_saved_scenarios }}</p>
|
||||
<p class="text-3xl font-bold text-navy metric">{{ stats.scenarios }}</p>
|
||||
<p class="text-xs text-slate mt-1">No limits</p>
|
||||
<p class="text-xs text-slate mt-1">{{ t.dash_no_limits }}</p>
|
||||
</div>
|
||||
<div class="card text-center">
|
||||
<p class="card-header">Lead Requests</p>
|
||||
<p class="card-header">{{ t.dash_lead_requests }}</p>
|
||||
<p class="text-3xl font-bold text-navy metric">{{ stats.leads }}</p>
|
||||
<p class="text-xs text-slate mt-1">Supplier & financing inquiries</p>
|
||||
<p class="text-xs text-slate mt-1">{{ t.dash_lead_requests_sub }}</p>
|
||||
</div>
|
||||
<div class="card text-center">
|
||||
<p class="card-header">Plan</p>
|
||||
<p class="text-3xl font-bold text-navy">Free</p>
|
||||
<p class="text-xs text-slate mt-1">Full access to all features</p>
|
||||
<p class="card-header">{{ t.dash_plan }}</p>
|
||||
<p class="text-3xl font-bold text-navy">{{ t.dash_plan_free }}</p>
|
||||
<p class="text-xs text-slate mt-1">{{ t.dash_plan_free_sub }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="text-xl mb-4">Quick Actions</h2>
|
||||
<h2 class="text-xl mb-4">{{ t.dash_quick_actions }}</h2>
|
||||
<div class="grid-3">
|
||||
<a href="{{ url_for('planner.index') }}" class="btn text-center">Open Planner</a>
|
||||
<a href="{{ url_for('leads.suppliers') }}" class="btn-outline text-center">Get Supplier Quotes</a>
|
||||
<a href="{{ url_for('dashboard.settings') }}" class="btn-outline text-center">Settings</a>
|
||||
<a href="{{ url_for('planner.index') }}" class="btn text-center">{{ t.dash_open_planner }}</a>
|
||||
<a href="{{ url_for('leads.suppliers') }}" class="btn-outline text-center">{{ t.dash_get_quotes }}</a>
|
||||
<a href="{{ url_for('dashboard.settings') }}" class="btn-outline text-center">{{ t.dash_settings }}</a>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,43 +1,43 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Settings - {{ config.APP_NAME }}{% endblock %}
|
||||
{% block title %}{{ t.dash_settings_title }} - {{ config.APP_NAME }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="container-page py-12">
|
||||
<h1 class="text-2xl mb-8">Settings</h1>
|
||||
<h1 class="text-2xl mb-8">{{ t.dash_settings_h1 }}</h1>
|
||||
|
||||
<section class="mb-10">
|
||||
<h2 class="text-xl mb-4">Profile</h2>
|
||||
<h2 class="text-xl mb-4">{{ t.dash_profile }}</h2>
|
||||
<div class="card">
|
||||
<form method="post" class="space-y-4">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<div>
|
||||
<label for="email" class="form-label">Email</label>
|
||||
<label for="email" class="form-label">{{ t.dash_email_label }}</label>
|
||||
<input type="email" id="email" value="{{ user.email }}" class="form-input bg-soft-white" disabled>
|
||||
<p class="form-hint">Email cannot be changed</p>
|
||||
<p class="form-hint">{{ t.dash_email_hint }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="name" class="form-label">Name</label>
|
||||
<input type="text" id="name" name="name" value="{{ user.name or '' }}" placeholder="Your name" class="form-input">
|
||||
<label for="name" class="form-label">{{ t.dash_name_label }}</label>
|
||||
<input type="text" id="name" name="name" value="{{ user.name or '' }}" placeholder="{{ t.dash_name_placeholder }}" class="form-input">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn">Save Changes</button>
|
||||
<button type="submit" class="btn">{{ t.dash_save_changes }}</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="text-xl mb-4">Danger Zone</h2>
|
||||
<h2 class="text-xl mb-4">{{ t.dash_danger_zone }}</h2>
|
||||
<div class="card border-danger/30">
|
||||
<p class="text-slate-dark mb-4">Once you delete your account, there is no going back.</p>
|
||||
<p class="text-slate-dark mb-4">{{ t.dash_delete_warning }}</p>
|
||||
|
||||
<details>
|
||||
<summary class="cursor-pointer text-sm font-semibold text-danger">Delete Account</summary>
|
||||
<p class="text-sm text-slate-dark mt-3 mb-3">This will delete all your scenarios and data permanently.</p>
|
||||
<summary class="cursor-pointer text-sm font-semibold text-danger">{{ t.dash_delete_account }}</summary>
|
||||
<p class="text-sm text-slate-dark mt-3 mb-3">{{ t.dash_delete_confirm }}</p>
|
||||
<form method="post" action="{{ url_for('dashboard.delete_account') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn-danger btn-sm">Yes, Delete My Account</button>
|
||||
<button type="submit" class="btn-danger btn-sm">{{ t.dash_delete_btn }}</button>
|
||||
</form>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{% if lang == 'de' %}Padel-Platz Anbieterverzeichnis - {{ config.APP_NAME }}{% else %}Padel Court Supplier Directory - {{ config.APP_NAME }}{% endif %}{% endblock %}
|
||||
{% block title %}{{ t.dir_page_title }} - {{ config.APP_NAME }}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
{% if lang == 'de' %}
|
||||
<meta name="description" content="Über {{ total_suppliers }}+ Anbieter aus {{ total_countries }} Ländern. Hersteller, Baufirmen, Kunstrasenproduzenten, Beleuchtung und Software. Den richtigen Partner für dein Projekt finden.">
|
||||
<meta property="og:title" content="Padel-Platz Anbieterverzeichnis - {{ config.APP_NAME }}">
|
||||
<meta property="og:description" content="Über {{ total_suppliers }}+ Anbieter aus {{ total_countries }} Ländern. Hersteller, Baufirmen, Kunstrasenproduzenten, Beleuchtung und Software.">
|
||||
{% else %}
|
||||
<meta name="description" content="Browse {{ total_suppliers }}+ padel court suppliers across {{ total_countries }} countries. Manufacturers, builders, turf, lighting, and software. Find the right partner for your project.">
|
||||
<meta property="og:title" content="Padel Court Supplier Directory - {{ config.APP_NAME }}">
|
||||
<meta property="og:description" content="Browse {{ total_suppliers }}+ padel court suppliers across {{ total_countries }} countries. Manufacturers, builders, turf, lighting, and software.">
|
||||
{% endif %}
|
||||
<meta name="description" content="{{ t.dir_page_meta_desc | tformat(count=total_suppliers, countries=total_countries) }}">
|
||||
<meta property="og:title" content="{{ t.dir_page_title }} - {{ config.APP_NAME }}">
|
||||
<meta property="og:description" content="{{ t.dir_page_og_desc | tformat(count=total_suppliers, countries=total_countries) }}">
|
||||
<style>
|
||||
:root {
|
||||
--dir-green: #15803D;
|
||||
@@ -305,7 +299,7 @@
|
||||
<main class="container-page">
|
||||
<div class="dir-hero">
|
||||
<h1>{{ t.dir_heading }}</h1>
|
||||
<p>{% if lang == 'de' %}Über {{ total_suppliers }}+ Anbieter aus {{ total_countries }} Ländern. Hersteller, Baufirmen und Spezialisten für dein Projekt.{% else %}Browse {{ total_suppliers }}+ suppliers across {{ total_countries }} countries. Find manufacturers, builders, and specialists for your project.{% endif %}</p>
|
||||
<p>{{ t.dir_subheading | tformat(n=total_suppliers, c=total_countries) }}</p>
|
||||
<div class="dir-stats">
|
||||
<span><strong>{{ total_suppliers }}</strong> {{ t.dir_stat_suppliers }}</span>
|
||||
<span><strong>{{ total_countries }}</strong> {{ t.dir_stat_countries }}</span>
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
<p style="font-weight:700;color:#16A34A;margin-bottom:4px">{{ t.enquiry_success_title }}</p>
|
||||
<p style="font-size:0.8125rem;color:#166534">
|
||||
{% if supplier and supplier.contact_email %}
|
||||
{% if lang == 'de' %}Deine Nachricht wurde an {{ supplier.name }} weitergeleitet. Der Anbieter meldet sich direkt bei dir.{% else %}We've forwarded your message to {{ supplier.name }}. They'll be in touch directly.{% endif %}
|
||||
{{ t.enquiry_forwarded_msg | tformat(name=supplier.name) }}
|
||||
{% else %}
|
||||
{% if lang == 'de' %}Deine Nachricht wurde empfangen. Das Team meldet sich in Kürze bei dir.{% else %}Your message has been received. The team will be in touch shortly.{% endif %}
|
||||
{{ t.enquiry_received_msg }}
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<p style="font-size:0.8125rem;color:#64748B;margin-bottom:1rem">
|
||||
{% if lang == 'de' %}{{ suppliers|length }} von {{ total }} Anbieter{% if total != 1 %}n{% endif %}{% else %}Showing {{ suppliers|length }} of {{ total }} supplier{{ 's' if total != 1 }}{% endif %}
|
||||
{% if total != 1 %}{{ t.dir_results_count_plural | tformat(shown=suppliers|length, total=total) }}{% else %}{{ t.dir_results_count_singular | tformat(shown=suppliers|length, total=total) }}{% endif %}
|
||||
{% if page > 1 %} (page {{ page }}){% endif %}
|
||||
</p>
|
||||
|
||||
@@ -38,19 +38,19 @@
|
||||
<div class="ph-grid"></div>
|
||||
<div class="example-icon">
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
|
||||
<p>{% if lang == 'de' %}Dein Projektfoto{% else %}Your project photo{% endif %}</p>
|
||||
<p>{{ t.dir_ex_photo }}</p>
|
||||
</div>
|
||||
<div class="dir-card__featured" style="background:#3B82F6">{% if lang == 'de' %}Beispiel{% else %}Example{% endif %}</div>
|
||||
<div class="dir-card__cat dir-card__cat--example">{% if lang == 'de' %}Deine Kategorie{% else %}Your Category{% endif %}</div>
|
||||
<div class="dir-card__featured" style="background:#3B82F6">{{ t.dir_ex_badge }}</div>
|
||||
<div class="dir-card__cat dir-card__cat--example">{{ t.dir_ex_category }}</div>
|
||||
</div>
|
||||
<div class="dir-card__body">
|
||||
<div class="dir-card__logo-wrap">
|
||||
<div class="dir-card__logo-ph">?</div>
|
||||
</div>
|
||||
<h3 class="dir-card__name">{% if lang == 'de' %}Dein Unternehmen{% else %}Your Company{% endif %}</h3>
|
||||
<h3 class="dir-card__name">{{ t.dir_ex_company }}</h3>
|
||||
<p class="dir-card__loc">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z"/><circle cx="12" cy="10" r="3"/></svg>
|
||||
{% if lang == 'de' %}Deine Stadt, Land{% else %}Your City, Country{% endif %}
|
||||
{{ t.dir_ex_location }}
|
||||
</p>
|
||||
<div class="dir-card__stats">
|
||||
<span class="dir-card__stat dir-card__stat--verified">
|
||||
@@ -59,10 +59,10 @@
|
||||
</span>
|
||||
<span class="dir-card__stat">12 projects · 8 yrs</span>
|
||||
</div>
|
||||
<p class="dir-card__desc">{% if lang == 'de' %}Verifizierte Einträge enthalten Titelfoto, Projektstatistiken und einen direkten Anfragebutton — sie erscheinen über nicht verifizierten Anbietern in den Suchergebnissen.{% else %}Verified listings include cover photo, project stats, and a direct quote button — placed above unverified suppliers in search results.{% endif %}</p>
|
||||
<p class="dir-card__desc">{{ t.dir_ex_desc }}</p>
|
||||
<div class="dir-card__foot">
|
||||
<span></span>
|
||||
<span class="dir-card__action dir-card__action--example">{% if lang == 'de' %}Eintrag erstellen →{% else %}Get listed →{% endif %}</span>
|
||||
<span class="dir-card__action dir-card__action--example">{{ t.dir_ex_cta }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{% if lang == 'de' %}{{ supplier.name }} - Anbieterverzeichnis - {{ config.APP_NAME }}{% else %}{{ supplier.name }} - Supplier Directory - {{ config.APP_NAME }}{% endif %}{% endblock %}
|
||||
{% block title %}{{ supplier.name }} - {{ t.dir_page_title }} - {{ config.APP_NAME }}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
{% set _sup_country = country_labels.get(supplier.country_code, supplier.country_code) %}
|
||||
@@ -361,7 +361,7 @@
|
||||
<div class="sp-enquiry-field">
|
||||
<label class="sp-enquiry-label">{{ t.sp_enquiry_message }} <span style="color:#EF4444">*</span></label>
|
||||
<textarea name="message" class="sp-enquiry-input sp-enquiry-textarea" required
|
||||
placeholder="{% if lang == 'de' %}Erzähl {{ supplier.name }} von Deinem Projekt…{% else %}Tell {{ supplier.name }} about your project…{% endif %}"></textarea>
|
||||
placeholder="{{ t.sp_enquiry_placeholder | tformat(name=supplier.name) }}"></textarea>
|
||||
</div>
|
||||
<button type="submit" class="sp-enquiry-submit">{{ t.sp_enquiry_submit }}</button>
|
||||
</form>
|
||||
@@ -461,10 +461,10 @@
|
||||
<div class="sp-cta-strip">
|
||||
<div class="sp-cta-strip__text">
|
||||
<h3>{{ t.sp_cta_basic_h3 }}</h3>
|
||||
<p>{% if lang == 'de' %}Upgrade auf Growth, um in unserer Anbieter-Vermittlung zu erscheinen und qualifizierte Projekt-Leads zu erhalten.{% else %}Upgrade to Growth to appear in our supplier matching and receive qualified project leads.{% endif %}</p>
|
||||
<p>{{ t.sp_cta_basic_desc }}</p>
|
||||
</div>
|
||||
<a href="{{ url_for('suppliers.signup') }}" class="sp-cta-strip__btn">
|
||||
{% if lang == 'de' %}Auf Growth upgraden →{% else %}Upgrade to Growth →{% endif %}
|
||||
{{ t.sp_cta_basic_btn }}
|
||||
</a>
|
||||
</div>
|
||||
{% elif supplier.tier == 'growth' %}
|
||||
@@ -500,7 +500,7 @@
|
||||
<div class="sp-locked-popover" id="locked-popover" role="tooltip">
|
||||
<p class="sp-locked-popover__title">{{ t.sp_locked_popover_title }}</p>
|
||||
<p class="sp-locked-popover__body">
|
||||
{% if lang == 'de' %}Dieser Anbieter hat seinen Eintrag noch nicht verifiziert. Nutze unseren Angebotsassistenten und wir vermitteln dich mit verifizierten Anbietern in Deiner Region.{% else %}This supplier hasn't verified their listing yet. Use our quote wizard and we'll match you with verified suppliers in your region.{% endif %}
|
||||
{{ t.sp_locked_popover_desc }}
|
||||
</p>
|
||||
<a href="{{ url_for('leads.quote_request', country=supplier.country_code) }}"
|
||||
class="sp-locked-popover__link">{{ t.sp_locked_popover_link }}</a>
|
||||
@@ -514,7 +514,7 @@
|
||||
<div class="sp-cta-strip">
|
||||
<div class="sp-cta-strip__text">
|
||||
<h3>{{ t.sp_cta_claim_h3 }}</h3>
|
||||
<p>{% if lang == 'de' %}Beanspruche und verifiziere diesen Eintrag, um Projektanfragen von Padel-Entwicklern zu erhalten.{% else %}Claim and verify this listing to start receiving project enquiries from padel developers.{% endif %}</p>
|
||||
<p>{{ t.sp_cta_claim_desc }}</p>
|
||||
</div>
|
||||
<a href="{{ url_for('suppliers.claim', slug=supplier.slug) }}" class="sp-cta-strip__btn sp-cta-strip__btn--green">
|
||||
{{ t.sp_cta_claim_btn }}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,7 @@
|
||||
|
||||
<div class="q-field-group">
|
||||
<span class="q-label">{{ t.q1_facility_label }} <span class="required">*</span></span>
|
||||
{% if 'facility_type' in errors %}<p class="q-error-hint">{% if lang == 'de' %}Bitte wähle einen Anlagentyp{% else %}Please select a facility type{% endif %}</p>{% endif %}
|
||||
{% if 'facility_type' in errors %}<p class="q-error-hint">{{ t.q1_error_facility }}</p>{% endif %}
|
||||
<div class="q-pills">
|
||||
{% for val, label in [('indoor', t.q1_facility_indoor), ('outdoor', t.q1_facility_outdoor), ('both', t.q1_facility_both)] %}
|
||||
<label><input type="radio" name="facility_type" value="{{ val }}" {{ 'checked' if data.get('facility_type') == val }}><span class="q-pill">{{ label }}</span></label>
|
||||
@@ -57,7 +57,7 @@
|
||||
<div id="q-progress" hx-swap-oob="innerHTML">
|
||||
<div class="q-progress__meta">
|
||||
<span class="q-progress__label">{{ steps[step - 1].title }}</span>
|
||||
<span class="q-progress__count">{% if lang == 'de' %}Schritt {{ step }} von {{ steps|length }}{% else %}{{ step }} of {{ steps|length }}{% endif %}</span>
|
||||
<span class="q-progress__count">{{ t.q_step_counter | tformat(step=step, total=steps|length) }}</span>
|
||||
</div>
|
||||
<div class="q-progress__track">
|
||||
<div class="q-progress__fill" style="width: {{ (step / steps|length * 100) | round }}%"></div>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<div class="q-field-group">
|
||||
<label class="q-label" for="country">{{ t.q2_country_label }} <span class="required">*</span></label>
|
||||
{% if 'country' in errors %}<p class="q-error-hint">{% if lang == 'de' %}Bitte wähle ein Land{% else %}Please select a country{% endif %}</p>{% endif %}
|
||||
{% if 'country' in errors %}<p class="q-error-hint">{{ t.q2_error_country }}</p>{% endif %}
|
||||
<select id="country" name="country" class="q-input {% if 'country' in errors %}q-input--error{% endif %}">
|
||||
<option value="">{{ t.q2_country_default }}</option>
|
||||
{% for code, name in [('DE', 'Germany'), ('ES', 'Spain'), ('IT', 'Italy'), ('FR', 'France'), ('NL', 'Netherlands'), ('SE', 'Sweden'), ('UK', 'United Kingdom'), ('PT', 'Portugal'), ('BE', 'Belgium'), ('AT', 'Austria'), ('CH', 'Switzerland'), ('DK', 'Denmark'), ('FI', 'Finland'), ('NO', 'Norway'), ('PL', 'Poland'), ('CZ', 'Czech Republic'), ('AE', 'UAE'), ('SA', 'Saudi Arabia'), ('US', 'United States'), ('OTHER', 'Other')] %}
|
||||
@@ -34,7 +34,7 @@
|
||||
<div id="q-progress" hx-swap-oob="innerHTML">
|
||||
<div class="q-progress__meta">
|
||||
<span class="q-progress__label">{{ steps[step - 1].title }}</span>
|
||||
<span class="q-progress__count">{% if lang == 'de' %}Schritt {{ step }} von {{ steps|length }}{% else %}{{ step }} of {{ steps|length }}{% endif %}</span>
|
||||
<span class="q-progress__count">{{ t.q_step_counter | tformat(step=step, total=steps|length) }}</span>
|
||||
</div>
|
||||
<div class="q-progress__track">
|
||||
<div class="q-progress__fill" style="width: {{ (step / steps|length * 100) | round }}%"></div>
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<div id="q-progress" hx-swap-oob="innerHTML">
|
||||
<div class="q-progress__meta">
|
||||
<span class="q-progress__label">{{ steps[step - 1].title }}</span>
|
||||
<span class="q-progress__count">{% if lang == 'de' %}Schritt {{ step }} von {{ steps|length }}{% else %}{{ step }} of {{ steps|length }}{% endif %}</span>
|
||||
<span class="q-progress__count">{{ t.q_step_counter | tformat(step=step, total=steps|length) }}</span>
|
||||
</div>
|
||||
<div class="q-progress__track">
|
||||
<div class="q-progress__fill" style="width: {{ (step / steps|length * 100) | round }}%"></div>
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<div id="q-progress" hx-swap-oob="innerHTML">
|
||||
<div class="q-progress__meta">
|
||||
<span class="q-progress__label">{{ steps[step - 1].title }}</span>
|
||||
<span class="q-progress__count">{% if lang == 'de' %}Schritt {{ step }} von {{ steps|length }}{% else %}{{ step }} of {{ steps|length }}{% endif %}</span>
|
||||
<span class="q-progress__count">{{ t.q_step_counter | tformat(step=step, total=steps|length) }}</span>
|
||||
</div>
|
||||
<div class="q-progress__track">
|
||||
<div class="q-progress__fill" style="width: {{ (step / steps|length * 100) | round }}%"></div>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<div class="q-field-group">
|
||||
<span class="q-label">{{ t.q5_timeline_label }} <span class="required">*</span></span>
|
||||
{% if 'timeline' in errors %}<p class="q-error-hint">{% if lang == 'de' %}Bitte wähle einen Zeitplan{% else %}Please select a timeline{% endif %}</p>{% endif %}
|
||||
{% if 'timeline' in errors %}<p class="q-error-hint">{{ t.q5_error_timeline }}</p>{% endif %}
|
||||
<div class="q-pills">
|
||||
{% for val, label in [('asap', t.q5_timeline_asap), ('3-6mo', t.q5_timeline_3_6), ('6-12mo', t.q5_timeline_6_12), ('12+mo', t.q5_timeline_12_plus)] %}
|
||||
<label><input type="radio" name="timeline" value="{{ val }}" {{ 'checked' if data.get('timeline') == val }}><span class="q-pill">{{ label }}</span></label>
|
||||
@@ -33,7 +33,7 @@
|
||||
<div id="q-progress" hx-swap-oob="innerHTML">
|
||||
<div class="q-progress__meta">
|
||||
<span class="q-progress__label">{{ steps[step - 1].title }}</span>
|
||||
<span class="q-progress__count">{% if lang == 'de' %}Schritt {{ step }} von {{ steps|length }}{% else %}{{ step }} of {{ steps|length }}{% endif %}</span>
|
||||
<span class="q-progress__count">{{ t.q_step_counter | tformat(step=step, total=steps|length) }}</span>
|
||||
</div>
|
||||
<div class="q-progress__track">
|
||||
<div class="q-progress__fill" style="width: {{ (step / steps|length * 100) | round }}%"></div>
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
<div id="q-progress" hx-swap-oob="innerHTML">
|
||||
<div class="q-progress__meta">
|
||||
<span class="q-progress__label">{{ steps[step - 1].title }}</span>
|
||||
<span class="q-progress__count">{% if lang == 'de' %}Schritt {{ step }} von {{ steps|length }}{% else %}{{ step }} of {{ steps|length }}{% endif %}</span>
|
||||
<span class="q-progress__count">{{ t.q_step_counter | tformat(step=step, total=steps|length) }}</span>
|
||||
</div>
|
||||
<div class="q-progress__track">
|
||||
<div class="q-progress__fill" style="width: {{ (step / steps|length * 100) | round }}%"></div>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<div class="q-field-group">
|
||||
<span class="q-label">{{ t.q7_role_label }} <span class="required">*</span></span>
|
||||
{% if 'stakeholder_type' in errors %}<p class="q-error-hint">{% if lang == 'de' %}Bitte wähle Deine Rolle{% else %}Please select your role{% endif %}</p>{% endif %}
|
||||
{% if 'stakeholder_type' in errors %}<p class="q-error-hint">{{ t.q7_error_role }}</p>{% endif %}
|
||||
<div class="q-pills">
|
||||
{% for val, label in [('entrepreneur', t.q7_role_entrepreneur), ('tennis_club', t.q7_role_tennis), ('municipality', t.q7_role_municipality), ('developer', t.q7_role_developer), ('operator', t.q7_role_operator), ('architect', t.q7_role_architect)] %}
|
||||
<label><input type="radio" name="stakeholder_type" value="{{ val }}" {{ 'checked' if data.get('stakeholder_type') == val }}><span class="q-pill">{{ label }}</span></label>
|
||||
@@ -37,7 +37,7 @@
|
||||
<div id="q-progress" hx-swap-oob="innerHTML">
|
||||
<div class="q-progress__meta">
|
||||
<span class="q-progress__label">{{ steps[step - 1].title }}</span>
|
||||
<span class="q-progress__count">{% if lang == 'de' %}Schritt {{ step }} von {{ steps|length }}{% else %}{{ step }} of {{ steps|length }}{% endif %}</span>
|
||||
<span class="q-progress__count">{{ t.q_step_counter | tformat(step=step, total=steps|length) }}</span>
|
||||
</div>
|
||||
<div class="q-progress__track">
|
||||
<div class="q-progress__fill" style="width: {{ (step / steps|length * 100) | round }}%"></div>
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
<div id="q-progress" hx-swap-oob="innerHTML">
|
||||
<div class="q-progress__meta">
|
||||
<span class="q-progress__label">{{ steps[step - 1].title }}</span>
|
||||
<span class="q-progress__count">{% if lang == 'de' %}Schritt {{ step }} von {{ steps|length }}{% else %}{{ step }} of {{ steps|length }}{% endif %}</span>
|
||||
<span class="q-progress__count">{{ t.q_step_counter | tformat(step=step, total=steps|length) }}</span>
|
||||
</div>
|
||||
<div class="q-progress__track">
|
||||
<div class="q-progress__fill" style="width: {{ (step / steps|length * 100) | round }}%"></div>
|
||||
|
||||
@@ -21,19 +21,19 @@
|
||||
|
||||
<div class="q-field-group">
|
||||
<label class="q-label" for="contact_name">{{ t.q9_name_label }} <span class="required">*</span></label>
|
||||
{% if 'contact_name' in errors %}<p class="q-error-hint">{% if lang == 'de' %}Vollständiger Name ist erforderlich{% else %}Full name is required{% endif %}</p>{% endif %}
|
||||
{% if 'contact_name' in errors %}<p class="q-error-hint">{{ t.q9_error_name }}</p>{% endif %}
|
||||
<input type="text" id="contact_name" name="contact_name" class="q-input {% if 'contact_name' in errors %}q-input--error{% endif %}" value="{{ data.get('contact_name', '') }}" required>
|
||||
</div>
|
||||
|
||||
<div class="q-field-group">
|
||||
<label class="q-label" for="contact_email">{{ t.q9_email_label }} <span class="required">*</span></label>
|
||||
{% if 'contact_email' in errors %}<p class="q-error-hint">{% if lang == 'de' %}E-Mail ist erforderlich{% else %}Email is required{% endif %}</p>{% endif %}
|
||||
{% if 'contact_email' in errors %}<p class="q-error-hint">{{ t.q9_error_email }}</p>{% endif %}
|
||||
<input type="email" id="contact_email" name="contact_email" class="q-input {% if 'contact_email' in errors %}q-input--error{% endif %}" value="{{ data.get('contact_email', '') }}" required>
|
||||
</div>
|
||||
|
||||
<div class="q-field-group">
|
||||
<label class="q-label" for="contact_phone">{{ t.q9_phone_label }} <span class="required">*</span></label>
|
||||
{% if 'contact_phone' in errors %}<p class="q-error-hint">{% if lang == 'de' %}Telefonnummer ist erforderlich{% else %}Phone number is required{% endif %}</p>{% endif %}
|
||||
{% if 'contact_phone' in errors %}<p class="q-error-hint">{{ t.q9_error_phone }}</p>{% endif %}
|
||||
<input type="tel" id="contact_phone" name="contact_phone" class="q-input {% if 'contact_phone' in errors %}q-input--error{% endif %}" value="{{ data.get('contact_phone', '') }}" required>
|
||||
</div>
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
<div id="q-progress" hx-swap-oob="innerHTML">
|
||||
<div class="q-progress__meta">
|
||||
<span class="q-progress__label">{{ steps[step - 1].title }}</span>
|
||||
<span class="q-progress__count">{% if lang == 'de' %}Schritt {{ step }} von {{ steps|length }}{% else %}{{ step }} of {{ steps|length }}{% endif %}</span>
|
||||
<span class="q-progress__count">{{ t.q_step_counter | tformat(step=step, total=steps|length) }}</span>
|
||||
</div>
|
||||
<div class="q-progress__track">
|
||||
<div class="q-progress__fill" style="width: {{ (step / steps|length * 100) | round }}%"></div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{% if lang == 'de' %}Angebote von Bauunternehmen erhalten - {{ config.APP_NAME }}{% else %}Get Builder Quotes - {{ config.APP_NAME }}{% endif %}{% endblock %}
|
||||
{% block title %}{{ t.q_page_title }} - {{ config.APP_NAME }}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<style>
|
||||
@@ -105,7 +105,7 @@
|
||||
<div class="q-progress" id="q-progress">
|
||||
<div class="q-progress__meta">
|
||||
<span class="q-progress__label">{{ steps[step - 1].title }}</span>
|
||||
<span class="q-progress__count">{% if lang == 'de' %}Schritt {{ step }} von {{ steps|length }}{% else %}{{ step }} of {{ steps|length }}{% endif %}</span>
|
||||
<span class="q-progress__count">{{ t.q_step_counter | tformat(step=step, total=steps|length) }}</span>
|
||||
</div>
|
||||
<div class="q-progress__track">
|
||||
<div class="q-progress__fill" style="width: {{ (step / steps|length * 100) | round }}%"></div>
|
||||
|
||||
@@ -57,16 +57,12 @@
|
||||
|
||||
<h1 class="text-2xl" style="margin-bottom: 0.5rem;">{{ t.qs_title }}</h1>
|
||||
<p style="color: #64748B; font-size: 0.9375rem;">
|
||||
{% if lang == 'de' %}
|
||||
Wir haben Dein{% if court_count %} {{ court_count }}-Platz-{% endif %}{% if facility_type %} {{ facility_type }}-{% endif %}Projekt{% if country %} in {{ country }}{% endif %} mit verifizierten Anbietern abgestimmt, die sich mit maßgeschneiderten Angeboten bei Dir melden.
|
||||
{% else %}
|
||||
We've matched your
|
||||
{% if court_count %}{{ court_count }}-court{% endif %}
|
||||
{% if facility_type %}{{ facility_type }}{% endif %}
|
||||
project
|
||||
{{ t.qs_matched_pre }}
|
||||
{% if court_count %}{{ court_count }}{{ t.qs_matched_court_suffix }}{% endif %}
|
||||
{% if facility_type %}{{ t.qs_matched_facility_fmt | tformat(type=facility_type) }}{% endif %}
|
||||
{{ t.qs_matched_project }}
|
||||
{% if country %}in {{ country }}{% endif %}
|
||||
with verified suppliers who'll reach out with tailored proposals.
|
||||
{% endif %}
|
||||
{{ t.qs_matched_post }}
|
||||
</p>
|
||||
|
||||
<div class="next-steps">
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
|
||||
<h1 class="text-2xl mb-4">{{ t.qv_heading }}</h1>
|
||||
|
||||
<p class="text-slate-dark">{% if lang == 'de' %}Wir haben einen Verifizierungslink an folgende Adresse gesendet:{% else %}We've sent a verification link to:{% endif %}</p>
|
||||
<p class="text-slate-dark">{{ t.qv_sent_msg }}</p>
|
||||
<p class="font-semibold text-navy my-2">{{ contact_email }}</p>
|
||||
|
||||
<p class="text-slate text-sm" style="margin-top: 1rem;">
|
||||
{% if lang == 'de' %}Klick auf den Link in der E-Mail, um Deine Adresse zu bestätigen und Deine Angebotsanfrage zu aktivieren. Dadurch wird auch Dein {{ config.APP_NAME }}-Konto erstellt und du wirst automatisch angemeldet.{% else %}Click the link in the email to verify your address and activate your quote request. This will also create your {{ config.APP_NAME }} account and log you in automatically.{% endif %}
|
||||
{{ t.qv_instructions | tformat(app_name=config.APP_NAME) }}
|
||||
</p>
|
||||
|
||||
<p class="text-slate text-sm" style="margin-top: 0.5rem;">
|
||||
@@ -22,10 +22,10 @@
|
||||
<hr>
|
||||
|
||||
<details class="text-left">
|
||||
<summary class="cursor-pointer text-sm font-medium text-navy">{% if lang == 'de' %}E-Mail nicht erhalten?{% else %}Didn't receive the email?{% endif %}</summary>
|
||||
<summary class="cursor-pointer text-sm font-medium text-navy">{{ t.qv_no_email }}</summary>
|
||||
<ul class="list-disc pl-6 mt-2 space-y-1 text-sm text-slate-dark">
|
||||
<li>{{ t.qv_spam }}</li>
|
||||
<li>{% if lang == 'de' %}Stell sicher, dass <strong>{{ contact_email }}</strong> korrekt ist{% else %}Make sure <strong>{{ contact_email }}</strong> is correct{% endif %}</li>
|
||||
<li>{{ t.qv_check_email_pre }}<strong>{{ contact_email }}</strong>{{ t.qv_check_email_post }}</li>
|
||||
<li>{{ t.qv_wait }}</li>
|
||||
</ul>
|
||||
<p class="text-sm text-slate mt-3">
|
||||
|
||||
1536
padelnomics/src/padelnomics/locales/de.json
Normal file
1536
padelnomics/src/padelnomics/locales/de.json
Normal file
File diff suppressed because it is too large
Load Diff
1536
padelnomics/src/padelnomics/locales/en.json
Normal file
1536
padelnomics/src/padelnomics/locales/en.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -46,25 +46,9 @@
|
||||
<div class="exp-price">€99 <span>one-time</span></div>
|
||||
|
||||
<ul class="exp-features">
|
||||
{% if lang == 'de' %}
|
||||
<li>Zusammenfassung</li>
|
||||
<li>CAPEX-Aufschlüsselung</li>
|
||||
<li>5-Jahres GuV-Projektion</li>
|
||||
<li>12-Monats-Cashflow</li>
|
||||
<li>Finanzierungsstruktur</li>
|
||||
<li>Kennzahlen (IRR, MOIC, DSCR)</li>
|
||||
<li>Sensitivitätsanalyse</li>
|
||||
<li>Englisch oder Deutsch</li>
|
||||
{% else %}
|
||||
<li>Executive summary</li>
|
||||
<li>CAPEX breakdown</li>
|
||||
<li>5-year P&L projection</li>
|
||||
<li>12-month cash flow</li>
|
||||
<li>Financing structure</li>
|
||||
<li>Key metrics (IRR, MOIC, DSCR)</li>
|
||||
<li>Sensitivity analysis</li>
|
||||
<li>English or German</li>
|
||||
{% endif %}
|
||||
{% for key in ['planner_export_f1','planner_export_f2','planner_export_f3','planner_export_f4','planner_export_f5','planner_export_f6','planner_export_f7','planner_export_f8'] %}
|
||||
<li>{{ t[key] }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<div class="exp-form">
|
||||
|
||||
@@ -7,89 +7,30 @@
|
||||
<div class="card max-w-md mx-auto mt-8 text-center">
|
||||
<h1 class="text-2xl mb-4">{{ t.export_waitlist_title }}</h1>
|
||||
|
||||
{% if lang == 'de' %}
|
||||
<p class="text-slate-dark mb-6">Wir bereiten den Start unseres professionellen Businessplan-PDF-Exports vor. Du stehst bereits auf der Warteliste und wirst benachrichtigt, sobald es verfügbar ist.</p>
|
||||
<p class="text-slate-dark mb-6">{{ t.planner_ewl_intro }}</p>
|
||||
|
||||
<div class="bg-slate-50 border border-slate-200 rounded-lg p-4 mb-6 text-left">
|
||||
<h3 class="font-semibold text-navy text-sm mb-2">Was enthalten ist</h3>
|
||||
<h3 class="font-semibold text-navy text-sm mb-2">{{ t.planner_ewl_included_h3 }}</h3>
|
||||
<ul class="text-sm text-slate-dark space-y-1">
|
||||
{% for key in ['planner_ewl_f1','planner_ewl_f2','planner_ewl_f3','planner_ewl_f4'] %}
|
||||
<li class="flex items-start gap-2">
|
||||
<svg class="w-4 h-4 text-blue-600 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<span>Professioneller Businessplan (20+ Seiten als PDF)</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<svg class="w-4 h-4 text-blue-600 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<span>Finanzprojektionen mit Diagrammen</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<svg class="w-4 h-4 text-blue-600 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<span>Marktanalyse und Strategie</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<svg class="w-4 h-4 text-blue-600 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<span>Mehrsprachige Optionen</span>
|
||||
<span>{{ t[key] }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-slate mb-6">
|
||||
<p>Du erhältst bei unserem Launch eine E-Mail mit:</p>
|
||||
<p>{{ t.planner_ewl_notify_p }}</p>
|
||||
<ul class="mt-2 text-slate-dark">
|
||||
<li>• Frühem Zugang mit Sonderpreis</li>
|
||||
<li>• Launch-Rabatt</li>
|
||||
<li>• Vorrangiger Generierungswarteschlange</li>
|
||||
<li>• {{ t.planner_ewl_email_item1 }}</li>
|
||||
<li>• {{ t.planner_ewl_email_item2 }}</li>
|
||||
<li>• {{ t.planner_ewl_email_item3 }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-slate-dark mb-6">We're preparing to launch our professional business plan PDF export feature. You're already on the waitlist and will be notified as soon as it's ready.</p>
|
||||
|
||||
<div class="bg-slate-50 border border-slate-200 rounded-lg p-4 mb-6 text-left">
|
||||
<h3 class="font-semibold text-navy text-sm mb-2">What's Included</h3>
|
||||
<ul class="text-sm text-slate-dark space-y-1">
|
||||
<li class="flex items-start gap-2">
|
||||
<svg class="w-4 h-4 text-blue-600 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<span>Professional 20+ page business plan PDF</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<svg class="w-4 h-4 text-blue-600 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<span>Financial projections with charts</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<svg class="w-4 h-4 text-blue-600 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<span>Market analysis and strategy</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<svg class="w-4 h-4 text-blue-600 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<span>Multiple language options</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-slate mb-6">
|
||||
<p>You'll receive an email when we launch with:</p>
|
||||
<ul class="mt-2 text-slate-dark">
|
||||
<li>• Early access pricing</li>
|
||||
<li>• Launch day discount</li>
|
||||
<li>• Priority generation queue</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<a href="{{ url_for('planner.index') }}" class="btn w-full">{{ t.export_waitlist_btn }}</a>
|
||||
</div>
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
</table>
|
||||
|
||||
<div class="chart-container mt-4">
|
||||
<div class="chart-container__label">{% if lang == 'de' %}CAPEX-Aufschlüsselung{% else %}CAPEX Breakdown{% endif %}</div>
|
||||
<div class="chart-container__label">{{ t.planner_chart_capex }}</div>
|
||||
<div class="chart-h-56 chart-container__canvas">
|
||||
<canvas id="chartCapex"></canvas>
|
||||
</div>
|
||||
|
||||
@@ -25,19 +25,19 @@
|
||||
</div>
|
||||
|
||||
<div class="chart-container mb-4">
|
||||
<div class="chart-container__label">{% if lang == 'de' %}Monatlicher Netto-Cashflow (60 Monate){% else %}Monthly Net Cash Flow (60 Months){% endif %}</div>
|
||||
<div class="chart-container__label">{{ t.planner_chart_cf }}</div>
|
||||
<div class="chart-h-56 chart-container__canvas"><canvas id="chartCF"></canvas></div>
|
||||
</div>
|
||||
<script type="application/json" id="chartCF-data">{{ d.cf_chart | tojson }}</script>
|
||||
|
||||
<div class="chart-container mb-4">
|
||||
<div class="chart-container__label">{% if lang == 'de' %}Kumulierter Cashflow{% else %}Cumulative Cash Flow{% endif %}</div>
|
||||
<div class="chart-container__label">{{ t.planner_chart_cum }}</div>
|
||||
<div class="chart-h-48 chart-container__canvas"><canvas id="chartCum"></canvas></div>
|
||||
</div>
|
||||
<script type="application/json" id="chartCum-data">{{ d.cum_chart | tojson }}</script>
|
||||
|
||||
<div class="mb-section">
|
||||
<div class="section-header"><h3>{% if lang == 'de' %}Jahresübersicht{% else %}Annual Summary{% endif %}</h3></div>
|
||||
<div class="section-header"><h3>{{ t.planner_section_annual }}</h3></div>
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
@@ -25,11 +25,11 @@
|
||||
|
||||
<div class="grid-2 mb-4">
|
||||
<div class="chart-container">
|
||||
<div class="chart-container__label">{% if lang == 'de' %}Monatlicher Umsatzaufbau (Anlaufphase){% else %}Monthly Revenue Build-Up (Ramp Period){% endif %}</div>
|
||||
<div class="chart-container__label">{{ t.planner_chart_rev_ramp }}</div>
|
||||
<div class="chart-h-48 chart-container__canvas"><canvas id="chartRevRamp"></canvas></div>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<div class="chart-container__label">{% if lang == 'de' %}Stabilisierte monatliche GuV{% else %}Stabilized Monthly P&L{% endif %}</div>
|
||||
<div class="chart-container__label">{{ t.planner_chart_pl }}</div>
|
||||
<div class="chart-h-48 chart-container__canvas"><canvas id="chartPL"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -37,7 +37,7 @@
|
||||
<script type="application/json" id="chartPL-data">{{ d.pl_chart | tojson }}</script>
|
||||
|
||||
<div class="mb-section">
|
||||
<div class="section-header"><h3>{% if lang == 'de' %}Einnahmequellen (stabilisierter Monat){% else %}Revenue Streams (Stabilized Month){% endif %}</h3></div>
|
||||
<div class="section-header"><h3>{{ t.planner_section_revenue }}</h3></div>
|
||||
{% set streams = [
|
||||
(t.stream_court_rental, d.courtRevMonth - d.feeDeduction),
|
||||
(t.stream_equipment, d.racketRev + d.ballMargin),
|
||||
@@ -68,7 +68,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mb-section">
|
||||
<div class="section-header"><h3>{% if lang == 'de' %}Monatliche OPEX-Aufschlüsselung{% else %}Monthly OpEx Breakdown{% endif %}</h3></div>
|
||||
<div class="section-header"><h3>{{ t.planner_section_opex_breakdown }}</h3></div>
|
||||
<table class="data-table">
|
||||
<thead><tr><th>{{ t.th_item }}</th><th class="right">{{ t.th_monthly }}</th></tr></thead>
|
||||
<tbody>
|
||||
@@ -88,7 +88,7 @@
|
||||
|
||||
{% if s.venue == 'outdoor' %}
|
||||
<div class="mb-section season-section visible">
|
||||
<div class="section-header"><h3>{% if lang == 'de' %}Outdoor-Saisonalität{% else %}Outdoor Seasonality{% endif %}</h3></div>
|
||||
<div class="section-header"><h3>{{ t.planner_section_seasonality }}</h3></div>
|
||||
<div class="chart-container"><div class="chart-h-40 chart-container__canvas"><canvas id="chartSeason"></canvas></div></div>
|
||||
</div>
|
||||
<script type="application/json" id="chartSeason-data">{{ d.season_chart | tojson }}</script>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
<div class="grid-2 mb-4">
|
||||
<div class="chart-container">
|
||||
<div class="chart-container__label" style="font-size:10px">{% if lang == 'de' %}Exit-Bewertungs-Wasserfall{% else %}Exit Valuation Waterfall{% endif %}</div>
|
||||
<div class="chart-container__label" style="font-size:10px">{{ t.planner_chart_exit_waterfall }}</div>
|
||||
<div id="exitWaterfall" style="margin-top:10px">
|
||||
{% set wf_rows = [
|
||||
(t.wf_stab_ebitda, d.stabEbitda | int | fmt_currency, 'c-head'),
|
||||
@@ -45,14 +45,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<div class="chart-container__label">{% if lang == 'de' %}DSCR nach Jahr{% else %}DSCR by Year{% endif %}</div>
|
||||
<div class="chart-container__label">{{ t.planner_chart_dscr }}</div>
|
||||
<div class="chart-h-44 chart-container__canvas"><canvas id="chartDSCR"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="application/json" id="chartDSCR-data">{{ d.dscr_chart | tojson }}</script>
|
||||
|
||||
<div class="mb-section">
|
||||
<div class="section-header"><h3>{% if lang == 'de' %}Auslastungs-Sensitivität{% else %}Utilization Sensitivity{% endif %}</h3></div>
|
||||
<div class="section-header"><h3>{{ t.planner_section_util_sensitivity }}</h3></div>
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -78,7 +78,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mb-section">
|
||||
<div class="section-header"><h3>{% if lang == 'de' %}Preis-Sensitivität (bei Ziel-Auslastung){% else %}Pricing Sensitivity (at target utilization){% endif %}</h3></div>
|
||||
<div class="section-header"><h3>{{ t.planner_section_price_sensitivity }}</h3></div>
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{% if lang == 'de' %}Padel-Platz Finanzrechner - {{ config.APP_NAME }}{% else %}Padel Court Financial Planner - {{ config.APP_NAME }}{% endif %}{% endblock %}
|
||||
{% block title %}{{ t.planner_page_title }} - {{ config.APP_NAME }}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
{% if lang == 'de' %}
|
||||
<meta name="description" content="Plane deine Padel-Platz-Investition mit unserem Finanzrechner mit 60+ Variablen. CAPEX, Cashflow und ROI berechnen.">
|
||||
<meta property="og:title" content="Padel-Platz Finanzrechner - {{ config.APP_NAME }}">
|
||||
<meta property="og:description" content="Plane deine Padel-Platz-Investition mit unserem Finanzrechner mit 60+ Variablen. CAPEX, Cashflow und ROI berechnen.">
|
||||
{% else %}
|
||||
<meta name="description" content="Plan your padel court investment with our 60+ variable financial planner. Calculate ROI, CAPEX, cash flow, and more.">
|
||||
<meta property="og:title" content="Padel Court Financial Planner - {{ config.APP_NAME }}">
|
||||
<meta property="og:description" content="Plan your padel court investment with our 60+ variable financial planner. Calculate ROI, CAPEX, cash flow, and more.">
|
||||
{% endif %}
|
||||
<meta name="description" content="{{ t.planner_meta_desc }}">
|
||||
<meta property="og:title" content="{{ t.planner_page_title }} - {{ config.APP_NAME }}">
|
||||
<meta property="og:description" content="{{ t.planner_meta_desc }}">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:image" content="{{ url_for('static', filename='images/planner-screenshot.png', _external=True) }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/planner.css') }}">
|
||||
@@ -45,7 +39,7 @@
|
||||
{% block content %}
|
||||
<div class="planner-app">
|
||||
<header class="planner-header">
|
||||
<h1>{% if lang == 'de' %}Padel-Platz Finanzrechner{% else %}Padel Court Financial Planner{% endif %}</h1>
|
||||
<h1>{{ t.planner_page_title }}</h1>
|
||||
<span id="headerTag" class="planner-summary">{{ s.venue == 'indoor' and t.label_indoor or t.label_outdoor }} · {{ s.own == 'buy' and t.label_build_buy or t.label_rent }} · {{ d.totalCourts }} {{ t.label_courts }} · {{ d.capex | fmt_k }}</span>
|
||||
|
||||
{% if user %}
|
||||
@@ -109,16 +103,11 @@
|
||||
|
||||
<!-- Step 1: Venue -->
|
||||
<div class="wizard-step active" data-wiz="1">
|
||||
{% if lang == 'de' %}
|
||||
<h2 class="wizard-step__title">Dein Padel-Platz</h2>
|
||||
<p class="wizard-step__sub">Definiere den Typ des Padel-Platzes, den du planst.</p>
|
||||
{% else %}
|
||||
<h2 class="wizard-step__title">Your Venue</h2>
|
||||
<p class="wizard-step__sub">Define the type of facility you're planning to build.</p>
|
||||
{% endif %}
|
||||
<h2 class="wizard-step__title">{{ t.planner_step1_title }}</h2>
|
||||
<p class="wizard-step__sub">{{ t.planner_step1_sub }}</p>
|
||||
|
||||
<div class="mb-section">
|
||||
<label class="slider-group__label">{% if lang == 'de' %}Umgebung{% else %}Environment{% endif %}</label>
|
||||
<label class="slider-group__label">{{ t.planner_label_environment }}</label>
|
||||
<div class="toggle-group">
|
||||
<button type="button" class="toggle-btn {{ 'toggle-btn--active' if s.venue == 'indoor' }}"
|
||||
data-toggle="venue" data-val="indoor"
|
||||
@@ -131,7 +120,7 @@
|
||||
hx-target="#tab-content" hx-include="#planner-form"
|
||||
onclick="setToggle(this,'venue','outdoor')">{{ t.toggle_outdoor }}</button>
|
||||
</div>
|
||||
<label class="slider-group__label">{% if lang == 'de' %}Eigentumsmodell{% else %}Ownership Model{% endif %}</label>
|
||||
<label class="slider-group__label">{{ t.planner_label_ownership }}</label>
|
||||
<div class="toggle-group">
|
||||
<button type="button" class="toggle-btn {{ 'toggle-btn--active' if s.own == 'rent' }}"
|
||||
data-toggle="own" data-val="rent"
|
||||
@@ -163,19 +152,11 @@
|
||||
</div>
|
||||
|
||||
<div class="mb-section">
|
||||
{% if lang == 'de' %}
|
||||
<div class="section-header"><h3>Platzkonfiguration</h3></div>
|
||||
{% else %}
|
||||
<div class="section-header"><h3>Court Configuration</h3></div>
|
||||
{% endif %}
|
||||
<div class="section-header"><h3>{{ t.planner_section_court_config }}</h3></div>
|
||||
{{ slider('dblCourts', t.sl_dbl_courts, 0, 30, 1, s.dblCourts, t.tip_dbl_courts) }}
|
||||
{{ slider('sglCourts', t.sl_sgl_courts, 0, 30, 1, s.sglCourts, t.tip_sgl_courts) }}
|
||||
|
||||
{% if lang == 'de' %}
|
||||
<div class="section-header" style="margin-top:1rem"><h3>Platzbedarf</h3></div>
|
||||
{% else %}
|
||||
<div class="section-header" style="margin-top:1rem"><h3>Space Requirements</h3></div>
|
||||
{% endif %}
|
||||
<div class="section-header" style="margin-top:1rem"><h3>{{ t.planner_section_space_req }}</h3></div>
|
||||
<div data-show-venue="indoor">
|
||||
{{ slider('sqmPerDblHall', t.sl_sqm_dbl_hall, 200, 600, 10, s.sqmPerDblHall, t.tip_sqm_dbl_hall) }}
|
||||
{{ slider('sqmPerSglHall', t.sl_sqm_sgl_hall, 120, 400, 10, s.sqmPerSglHall, t.tip_sqm_sgl_hall) }}
|
||||
@@ -190,20 +171,11 @@
|
||||
|
||||
<!-- Step 2: Pricing & Utilization -->
|
||||
<div class="wizard-step" data-wiz="2">
|
||||
{% if lang == 'de' %}
|
||||
<h2 class="wizard-step__title">Preise & Auslastung</h2>
|
||||
<p class="wizard-step__sub">Lege Deine Platztarife, Betriebszeiten und Nebeneinnahmen fest.</p>
|
||||
{% else %}
|
||||
<h2 class="wizard-step__title">Pricing & Utilization</h2>
|
||||
<p class="wizard-step__sub">Set your court rates, operating schedule, and ancillary revenue streams.</p>
|
||||
{% endif %}
|
||||
<h2 class="wizard-step__title">{{ t.planner_step2_title }}</h2>
|
||||
<p class="wizard-step__sub">{{ t.planner_step2_sub }}</p>
|
||||
|
||||
<div class="mb-section">
|
||||
{% if lang == 'de' %}
|
||||
<div class="section-header"><h3>Preise</h3><span class="hint">Pro Platz und Stunde</span></div>
|
||||
{% else %}
|
||||
<div class="section-header"><h3>Pricing</h3><span class="hint">Per court per hour</span></div>
|
||||
{% endif %}
|
||||
<div class="section-header"><h3>{{ t.planner_section_pricing }}</h3><span class="hint">{{ t.planner_hint_per_court }}</span></div>
|
||||
{{ slider('ratePeak', t.sl_rate_peak, 0, 150, 1, s.ratePeak, t.tip_rate_peak) }}
|
||||
{{ slider('rateOffPeak', t.sl_rate_offpeak, 0, 150, 1, s.rateOffPeak, t.tip_rate_offpeak) }}
|
||||
{{ slider('rateSingle', t.sl_rate_single, 0, 150, 1, s.rateSingle, t.tip_rate_single) }}
|
||||
@@ -212,11 +184,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mb-section">
|
||||
{% if lang == 'de' %}
|
||||
<div class="section-header"><h3>Auslastung & Betrieb</h3></div>
|
||||
{% else %}
|
||||
<div class="section-header"><h3>Utilization & Operations</h3></div>
|
||||
{% endif %}
|
||||
<div class="section-header"><h3>{{ t.planner_section_util }}</h3></div>
|
||||
{{ slider('utilTarget', t.sl_util_target, 0, 100, 1, s.utilTarget, t.tip_util_target) }}
|
||||
{{ slider('hoursPerDay', t.sl_hours_per_day, 0, 24, 1, s.hoursPerDay, t.tip_hours_per_day) }}
|
||||
{{ slider('daysPerMonthIndoor', t.sl_days_indoor, 0, 31, 1, s.daysPerMonthIndoor, t.tip_days_indoor) }}
|
||||
@@ -231,20 +199,11 @@
|
||||
|
||||
<!-- Step 3: Investment & Build Costs -->
|
||||
<div class="wizard-step" data-wiz="3">
|
||||
{% if lang == 'de' %}
|
||||
<h2 class="wizard-step__title">Investition & Baukosten</h2>
|
||||
<p class="wizard-step__sub">Konfiguriere Baukosten, Glas- und Beleuchtungsoptionen sowie Dein Budgetziel.</p>
|
||||
{% else %}
|
||||
<h2 class="wizard-step__title">Investment & Build Costs</h2>
|
||||
<p class="wizard-step__sub">Configure construction costs, glass and lighting options, and your budget target.</p>
|
||||
{% endif %}
|
||||
<h2 class="wizard-step__title">{{ t.planner_step3_title }}</h2>
|
||||
<p class="wizard-step__sub">{{ t.planner_step3_sub }}</p>
|
||||
|
||||
<div class="mb-section">
|
||||
{% if lang == 'de' %}
|
||||
<div class="section-header"><h3>Bau & CAPEX</h3><span class="hint">Nach Szenario anpassen</span></div>
|
||||
{% else %}
|
||||
<div class="section-header"><h3>Construction & CAPEX</h3><span class="hint">Adjust per scenario</span></div>
|
||||
{% endif %}
|
||||
<div class="section-header"><h3>{{ t.planner_section_capex }}</h3><span class="hint">{{ t.planner_hint_adjust }}</span></div>
|
||||
|
||||
<div class="pill-group">
|
||||
<label><span class="slider-group__label">{{ t.pill_glass_type }}</span><span class="ti">i<span class="tp">{{ t.tip_glass_type }}</span></span></label>
|
||||
@@ -303,17 +262,11 @@
|
||||
|
||||
<!-- Step 4: Operations & Financing -->
|
||||
<div class="wizard-step" data-wiz="4">
|
||||
{% if lang == 'de' %}
|
||||
<h2 class="wizard-step__title">Betrieb & Finanzierung</h2>
|
||||
<p class="wizard-step__sub">Monatliche Betriebskosten, Kreditkonditionen und Exit-Annahmen.</p>
|
||||
{% else %}
|
||||
<h2 class="wizard-step__title">Operations & Financing</h2>
|
||||
<p class="wizard-step__sub">Monthly operating costs, loan terms, and exit assumptions.</p>
|
||||
{% endif %}
|
||||
<h2 class="wizard-step__title">{{ t.planner_step4_title }}</h2>
|
||||
<p class="wizard-step__sub">{{ t.planner_step4_sub }}</p>
|
||||
|
||||
<div class="mb-section">
|
||||
{% if lang == 'de' %}<div class="section-header"><h3>Monatliche Betriebskosten</h3></div>
|
||||
{% else %}<div class="section-header"><h3>Monthly Operating Costs</h3></div>{% endif %}
|
||||
<div class="section-header"><h3>{{ t.planner_section_opex }}</h3></div>
|
||||
|
||||
<div data-show-opex="indoor-rent">{{ slider('rentSqm', t.sl_rent_sqm, 0, 25, 0.5, s.rentSqm, t.tip_rent_sqm) }}</div>
|
||||
<div data-show-opex="outdoor-rent">{{ slider('outdoorRent', t.sl_outdoor_rent, 0, 5000, 50, s.outdoorRent, t.tip_outdoor_rent) }}</div>
|
||||
@@ -336,8 +289,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mb-section">
|
||||
{% if lang == 'de' %}<div class="section-header"><h3>Finanzierung</h3></div>
|
||||
{% else %}<div class="section-header"><h3>Financing</h3></div>{% endif %}
|
||||
<div class="section-header"><h3>{{ t.planner_section_financing }}</h3></div>
|
||||
{{ slider('loanPct', t.sl_loan_pct, 0, 100, 1, s.loanPct, t.tip_loan_pct) }}
|
||||
{{ slider('interestRate', t.sl_interest_rate, 0, 15, 0.1, s.interestRate, t.tip_interest_rate) }}
|
||||
{{ slider('loanTerm', t.sl_loan_term, 0, 30, 1, s.loanTerm, t.tip_loan_term) }}
|
||||
@@ -345,8 +297,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mb-section">
|
||||
{% if lang == 'de' %}<div class="section-header"><h3>Exit-Annahmen</h3></div>
|
||||
{% else %}<div class="section-header"><h3>Exit Assumptions</h3></div>{% endif %}
|
||||
<div class="section-header"><h3>{{ t.planner_section_exit }}</h3></div>
|
||||
{{ slider('holdYears', t.sl_hold_years, 1, 20, 1, s.holdYears, t.tip_hold_years) }}
|
||||
{{ slider('exitMultiple', t.sl_exit_multiple, 0, 20, 0.5, s.exitMultiple, t.tip_exit_multiple) }}
|
||||
{{ slider('annualRevGrowth', t.sl_annual_rev_growth, 0, 15, 0.5, s.annualRevGrowth, t.tip_annual_rev_growth) }}
|
||||
|
||||
@@ -1,46 +1,28 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{% if lang == 'de' %}Über Padelnomics — Planungsplattform für Padelplatz-Investitionen{% else %}About Padelnomics — Padel Court Investment Platform{% endif %}{% endblock %}
|
||||
{% block title %}{{ t.about_page_title }}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<meta name="description" content="{% if lang == 'de' %}Padelnomics ist eine kostenlose Finanzplanungsplattform für Padel-Unternehmer. Modelliere deine Investition, finde Anbieter und plane dein Padel-Business mit professionellen Tools.{% else %}Padelnomics is a free financial planning platform for padel entrepreneurs. Model your investment, find suppliers, and plan your padel court business with professional-grade tools.{% endif %}">
|
||||
<meta property="og:title" content="{% if lang == 'de' %}Über Padelnomics — Planungsplattform für Padelplatz-Investitionen{% else %}About Padelnomics — Padel Court Investment Platform{% endif %}">
|
||||
<meta property="og:description" content="{% if lang == 'de' %}Entwickelt für Padel-Unternehmer, die professionelle Finanztools ohne Beratungskosten benötigen. Kostenloser Planer, 60+ Variablen, Anbieterverzeichnis und mehr.{% else %}Built for padel entrepreneurs who need professional financial tools without consulting fees. Free planner, 60+ variables, supplier directory, and more.{% endif %}">
|
||||
<meta name="description" content="{{ t.about_meta_desc }}">
|
||||
<meta property="og:title" content="{{ t.about_page_title }}">
|
||||
<meta property="og:description" content="{{ t.about_og_desc }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="container-page py-12">
|
||||
<div class="card max-w-3xl mx-auto">
|
||||
<h1 class="text-2xl mb-6 text-center">
|
||||
{% if lang == 'de' %}Über {% endif %}{{ config.APP_NAME }}
|
||||
</h1>
|
||||
<h1 class="text-2xl mb-6 text-center">{{ t.about_h1 }}</h1>
|
||||
|
||||
<div class="space-y-4 text-slate-dark leading-relaxed">
|
||||
{% if lang == 'de' %}
|
||||
<p>Padel ist der am schnellsten wachsende Sport in Europa, doch für die meisten Unternehmer ist die Eröffnung einer Paddelhalle immer noch ein Sprung ins Ungewisse. Die Finanzen sind komplex: Der CAPEX variiert stark je nach Anlagentyp, der Standort bestimmt die Auslastung, und der Unterschied zwischen 60 % und 75 % Belegung kann über Erfolg oder Misserfolg einer Investition entscheiden.</p>
|
||||
|
||||
<p>Wir haben Padelnomics gebaut, weil wir kein ausreichend gutes Finanzplanungstool gefunden haben. Vorhandene Rechner sind entweder zu simpel (5 Eingaben, ein Ergebnis) oder hinter teuren Beratungsmandaten verborgen. Wir wollten etwas mit der Tiefe eines professionellen Finanzmodells, aber der Zugänglichkeit einer Web-App.</p>
|
||||
|
||||
<p>Das Ergebnis ist ein kostenloser Finanzplaner mit 60+ anpassbaren Variablen, 6 Analyse-Tabs, Sensitivitätsanalyse und den professionellen Kennzahlen, die Banken und Investoren sehen müssen. Jede Annahme ist transparent und anpassbar. Keine Blackboxen.</p>
|
||||
<p>{{ t.about_body_p1 }}</p>
|
||||
<p>{{ t.about_body_p2 }}</p>
|
||||
<p>{{ t.about_body_p3 }}</p>
|
||||
|
||||
<h3 class="text-lg mt-6">{{ t.about_why_h3 }}</h3>
|
||||
<p>Der Planer ist kostenlos, weil wir glauben, dass bessere Planung zu besseren Padelanlagen führt — und das ist gut für die gesamte Branche. Wir verdienen Geld, indem wir Unternehmer mit Platz-Anbietern und Finanzierungspartnern verbinden, wenn sie bereit sind, von der Planung zum Bau überzugehen.</p>
|
||||
<p>{{ t.about_why_p }}</p>
|
||||
|
||||
<h3 class="text-lg mt-6">{{ t.about_next_h3 }}</h3>
|
||||
<p>Padelnomics baut die Infrastruktur für Padel-Unternehmertum auf. Nach der Planung kommen Finanzierung, Bau und Betrieb. Wir arbeiten an Marktintelligenz auf Basis realer Buchungsdaten, einem Anbietermarktplatz für Platzausstattung und Analyse-Tools für Betreiber.</p>
|
||||
{% else %}
|
||||
<p>Padel is the fastest-growing sport in Europe, but opening a padel hall is still a leap of faith for most entrepreneurs. The financials are complex: CAPEX varies wildly depending on venue type, location drives utilization, and the difference between a 60% and 75% occupancy rate can mean the difference between a great investment and a money pit.</p>
|
||||
|
||||
<p>We built Padelnomics because we couldn't find a financial planning tool that was good enough. Existing calculators are either too simplistic (5 inputs, one output) or locked behind expensive consulting engagements. We wanted something with the depth of a professional financial model but the accessibility of a web app.</p>
|
||||
|
||||
<p>The result is a free financial planner with 60+ adjustable variables, 6 analysis tabs, sensitivity analysis, and the professional metrics that banks and investors need to see. Every assumption is transparent and adjustable. No black boxes.</p>
|
||||
|
||||
<h3 class="text-lg mt-6">{{ t.about_why_h3 }}</h3>
|
||||
<p>The planner is free because we believe better planning leads to better padel venues, and that's good for the entire industry. We make money by connecting entrepreneurs with court suppliers and financing partners when they're ready to move from planning to building.</p>
|
||||
|
||||
<h3 class="text-lg mt-6">{{ t.about_next_h3 }}</h3>
|
||||
<p>Padelnomics is building the infrastructure for padel entrepreneurship. After planning comes financing, building, and operating. We're working on market intelligence powered by real booking data, a supplier marketplace for court equipment, and analytics tools for venue operators.</p>
|
||||
{% endif %}
|
||||
<p>{{ t.about_next_p }}</p>
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-10">
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{% if lang == 'de' %}Funktionen - Padel-Kostenrechner & Finanzplaner | {{ config.APP_NAME }}{% else %}Features - Padel Court Financial Planner | {{ config.APP_NAME }}{% endif %}{% endblock %}
|
||||
{% block title %}{{ t.features_title_prefix }} | {{ config.APP_NAME }}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<meta name="description" content="{% if lang == 'de' %}60+ anpassbare Variablen, 6 Analyse-Tabs, Sensitivitätsanalyse und professionelle Finanzprojektionen für deine Padelplatz-Investition.{% else %}60+ adjustable variables, 6 analysis tabs, sensitivity analysis, and professional-grade financial projections for your padel court investment.{% endif %}">
|
||||
<meta property="og:title" content="{% if lang == 'de' %}Funktionen - Padel-Kostenrechner & Finanzplaner | {{ config.APP_NAME }}{% else %}Features - Padel Court Financial Planner | {{ config.APP_NAME }}{% endif %}">
|
||||
<meta property="og:description" content="{% if lang == 'de' %}60+ anpassbare Variablen, 6 Analyse-Tabs, Sensitivitätsanalyse und professionelle Finanzprojektionen für deine Padelplatz-Investition.{% else %}60+ adjustable variables, 6 analysis tabs, sensitivity analysis, and professional-grade financial projections for your padel court investment.{% endif %}">
|
||||
<meta name="description" content="{{ t.features_meta_desc }}">
|
||||
<meta property="og:title" content="{{ t.features_title_prefix }} | {{ config.APP_NAME }}">
|
||||
<meta property="og:description" content="{{ t.features_meta_desc }}">
|
||||
<meta property="og:image" content="{{ url_for('static', filename='images/planner-screenshot.png', _external=True) }}">
|
||||
{% endblock %}
|
||||
|
||||
@@ -19,112 +19,52 @@
|
||||
<div class="grid-2">
|
||||
<div class="card">
|
||||
<h2 class="text-xl mb-2">{{ t.features_card_1_h2 }}</h2>
|
||||
<p class="text-slate-dark">
|
||||
{% if lang == 'de' %}
|
||||
Jede Annahme ist anpassbar: Platzbaukosten, Miete, Stundensätze, Auslastungskurven, Finanzierungskonditionen, Exit-Multiplikatoren. Nichts ist fest vorgegeben — Dein Modell spiegelt deine Realität wider.
|
||||
{% else %}
|
||||
Every assumption is adjustable. Court costs, rent, hourly pricing, utilization curves, financing terms, exit multiples. Nothing is hard-coded — your model reflects your reality.
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="text-slate-dark">{{ t.features_card_1_body }}</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2 class="text-xl mb-2">{{ t.features_card_2_h2 }}</h2>
|
||||
<p class="text-slate-dark">
|
||||
{% if lang == 'de' %}
|
||||
Annahmen, Investition (CAPEX), Betriebsmodell, Cashflow, Renditen & Exit sowie Kennzahlen. Jeder Tab mit interaktiven Diagrammen, die sich in Echtzeit aktualisieren, wenn du Eingaben anpasst.
|
||||
{% else %}
|
||||
Assumptions, Investment (CAPEX), Operating Model, Cash Flow, Returns & Exit, and Key Metrics. Each tab with interactive charts that update in real time as you adjust inputs.
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="text-slate-dark">{{ t.features_card_2_body }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-2">
|
||||
<div class="card">
|
||||
<h2 class="text-xl mb-2">{{ t.features_card_3_h2 }}</h2>
|
||||
<p class="text-slate-dark">
|
||||
{% if lang == 'de' %}
|
||||
Innenhallenmodelle (Anmietung eines Bestandsgebäudes oder Neubau) und Außenanlagen mit Saisonalitätsanpassungen. Szenarien direkt nebeneinander vergleichen, um den besten Ansatz für deinen Markt zu finden.
|
||||
{% else %}
|
||||
Model indoor halls (rent an existing building or build new) and outdoor courts with seasonality adjustments. Compare scenarios side by side to find the best approach for your market.
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="text-slate-dark">{{ t.features_card_3_body }}</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2 class="text-xl mb-2">{{ t.features_card_4_h2 }}</h2>
|
||||
<p class="text-slate-dark">
|
||||
{% if lang == 'de' %}
|
||||
Sieh dir an, wie sich deine IRR und Cash-Rendite bei unterschiedlichen Auslastungsraten und Preisen verändern. Ermittle deinen Break-even-Punkt sofort mit der integrierten Sensitivitätsmatrix.
|
||||
{% else %}
|
||||
See how your IRR and cash yield change across different utilization rates and pricing levels. Find your break-even point instantly with the built-in sensitivity matrix.
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="text-slate-dark">{{ t.features_card_4_body }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-2">
|
||||
<div class="card">
|
||||
<h2 class="text-xl mb-2">{{ t.features_card_5_h2 }}</h2>
|
||||
<p class="text-slate-dark">
|
||||
{% if lang == 'de' %}
|
||||
IRR, MOIC, DSCR, Cash-on-Cash-Rendite, Break-even-Auslastung, RevPAH, Schuldenrendite — die Kennzahlen, die Banken und Investoren in einem Padelplatz-Businessplan sehen möchten.
|
||||
{% else %}
|
||||
IRR, MOIC, DSCR, cash-on-cash yield, break-even utilization, RevPAH, debt yield. The metrics banks and investors expect to see in a padel court business plan.
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="text-slate-dark">{{ t.features_card_5_body }}</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2 class="text-xl mb-2">{{ t.features_card_6_h2 }}</h2>
|
||||
<p class="text-slate-dark">
|
||||
{% if lang == 'de' %}
|
||||
Unbegrenzte Szenarien speichern. Verschiedene Standorte, Platzzahlen, Finanzierungsstrukturen und Preisstrategien testen. Laden und vergleichen, um den optimalen Plan für deine Investition zu finden.
|
||||
{% else %}
|
||||
Save unlimited scenarios. Test different locations, court counts, financing structures, and pricing strategies. Load and compare to find the optimal plan for your investment.
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="text-slate-dark">{{ t.features_card_6_body }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6 max-w-3xl mx-auto mt-12">
|
||||
<div class="card">
|
||||
<h2 class="text-xl mb-2">{{ t.features_capex_h2 }}</h2>
|
||||
<p class="text-slate-dark">
|
||||
{% if lang == 'de' %}
|
||||
Jede Kostenstelle einzeln modellieren: Platzmontage, Bodenbelag, Beleuchtung, Klimatisierung, Umkleideräume, Rezeption, Parkplatz, Außenanlagen. Zwischen Gebäudenanmietung und Neubau umschalten. Grundstückskosten, Baukosten pro m² und Ausstattungsbudgets unabhängig voneinander anpassen.
|
||||
{% else %}
|
||||
Model every cost line individually: court installation, flooring, lighting, climate control, changing rooms, reception, parking, landscaping. Toggle between renting a building and constructing new. Adjust land costs, construction costs per sqm, and fit-out budgets independently.
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="text-slate-dark">{{ t.features_capex_body }}</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2 class="text-xl mb-2">{{ t.features_opex_h2 }}</h2>
|
||||
<p class="text-slate-dark">
|
||||
{% if lang == 'de' %}
|
||||
Peak- und Off-Peak-Preise mit konfigurierbaren Stundenaufteilungen. Monatliche Anlaufkurven für die Auslastung. Personalkosten, Wartung, Versicherung, Marketing und Betriebskosten — alle mit Schiebereglern anpassbar. Einnahmen aus Platzvermietung, Coaching, Ausrüstung und F&B.
|
||||
{% else %}
|
||||
Peak and off-peak pricing with configurable hour splits. Monthly utilization ramp-up curves. Staff costs, maintenance, insurance, marketing, and utilities — all adjustable with sliders. Revenue from court rentals, coaching, equipment, and F&B.
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="text-slate-dark">{{ t.features_opex_body }}</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2 class="text-xl mb-2">{{ t.features_cf_h2 }}</h2>
|
||||
<p class="text-slate-dark">
|
||||
{% if lang == 'de' %}
|
||||
Monatliche Cashflow-Projektionen über 10 Jahre. Eigen-/Fremdkapitalaufteilung, Zinssätze und Kreditlaufzeiten modellieren. Schuldendienstdeckungsgrade und freien Cashflow Monat für Monat einsehen. Wasserfalldiagramme zeigen genau, wohin dein Geld fließt.
|
||||
{% else %}
|
||||
10-year monthly cash flow projections. Model your equity/debt split, interest rates, and loan terms. See debt service coverage ratios and free cash flow month by month. Waterfall charts show exactly where your money goes.
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="text-slate-dark">{{ t.features_cf_body }}</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2 class="text-xl mb-2">{{ t.features_returns_h2 }}</h2>
|
||||
<p class="text-slate-dark">
|
||||
{% if lang == 'de' %}
|
||||
Eigenkapital-IRR und MOIC unter verschiedenen Exit-Szenarien berechnen. Cap-Rate-Exits mit konfigurierbaren Haltedauern modellieren. Die Eigenkapitalentwicklung vom Ersteinsatz bis zum Exit-Erlös nachvollziehen.
|
||||
{% else %}
|
||||
Calculate your equity IRR and MOIC under different exit scenarios. Model cap rate exits with configurable holding periods. See your equity waterfall from initial investment through to exit proceeds.
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="text-slate-dark">{{ t.features_returns_body }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{% if lang == 'de' %}Padelnomics - Padel-Kostenrechner & Finanzplaner{% else %}Padelnomics - Padel Court Business Plan & ROI Calculator{% endif %}{% endblock %}
|
||||
{% block title %}{{ t.landing_page_title }}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<meta name="description" content="{% if lang == 'de' %}Modelliere deine Padelplatz-Investition mit 60+ Variablen, Sensitivitätsanalyse und professionellen Projektionen. Innen-/Außenanlage, Miet- oder Eigentumsmodell.{% else %}Plan your padel court investment in minutes. 60+ variables, sensitivity analysis, and professional-grade projections. Indoor/outdoor, rent/buy models.{% endif %}">
|
||||
<meta property="og:title" content="Padelnomics - Padel Court Financial Planner">
|
||||
<meta property="og:description" content="{% if lang == 'de' %}Der professionellste Padel-Finanzplaner. 60+ Variablen, 6 Analyse-Tabs, Diagramme, Sensitivitätsanalyse und Anbieter-Vermittlung.{% else %}The most sophisticated padel court business plan calculator. 60+ variables, 6 analysis tabs, charts, sensitivity analysis, and supplier connections.{% endif %}">
|
||||
<meta name="description" content="{{ t.landing_meta_desc }}">
|
||||
<meta property="og:title" content="{{ t.landing_page_title }}">
|
||||
<meta property="og:description" content="{{ t.landing_og_desc }}">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:image" content="{{ url_for('static', filename='images/planner-screenshot.png', _external=True) }}">
|
||||
<style>
|
||||
@@ -250,14 +250,7 @@
|
||||
{{ t.landing_hero_h1_2 }}<br>
|
||||
<span class="accent">{{ t.landing_hero_h1_3 }}</span>
|
||||
</h1>
|
||||
<p class="hero-desc">
|
||||
{% if lang == 'de' %}
|
||||
Modelliere deine Padelplatz-Investition mit 60+ Variablen, Sensitivitätsanalyse und professionellen Projektionen. Dann wirst du mit verifizierten Anbietern zusammengebracht.
|
||||
{% else %}
|
||||
Model your padel court investment with 60+ variables, sensitivity analysis,
|
||||
and professional-grade projections. Then get matched with verified suppliers.
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="hero-desc">{{ t.landing_hero_desc }}</p>
|
||||
<div class="hero-actions">
|
||||
<a href="{{ url_for('planner.index') }}" class="btn-hero">{{ t.landing_hero_btn_primary }}</a>
|
||||
<a href="{{ url_for('directory.index') }}" class="btn-hero-outline">{{ t.landing_hero_btn_secondary }}</a>
|
||||
@@ -319,57 +312,27 @@
|
||||
<div class="journey-step journey-step--upcoming">
|
||||
<div class="journey-step__num">01</div>
|
||||
<h3 class="journey-step__title">{{ t.landing_journey_01 }} <span class="badge-soon">{{ t.landing_journey_01_badge }}</span></h3>
|
||||
<p class="journey-step__desc">
|
||||
{% if lang == 'de' %}
|
||||
Marktbedarfsanalyse, Standortbewertung und Identifikation von Nachfragepotenzialen.
|
||||
{% else %}
|
||||
Market demand analysis, whitespace mapping, location scoring.
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="journey-step__desc">{{ t.landing_journey_01_desc }}</p>
|
||||
</div>
|
||||
<div class="journey-step journey-step--active">
|
||||
<div class="journey-step__num">02</div>
|
||||
<h3 class="journey-step__title">{{ t.landing_journey_02 }}</h3>
|
||||
<p class="journey-step__desc">
|
||||
{% if lang == 'de' %}
|
||||
Modelliere deine Investition mit 60+ Variablen, Diagrammen und Sensitivitätsanalyse.
|
||||
{% else %}
|
||||
Model your investment with 60+ variables, charts, and sensitivity analysis.
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="journey-step__desc">{{ t.landing_journey_02_desc }}</p>
|
||||
</div>
|
||||
<div class="journey-step journey-step--upcoming">
|
||||
<div class="journey-step__num">03</div>
|
||||
<h3 class="journey-step__title">{{ t.landing_journey_03 }} <span class="badge-soon">{{ t.landing_journey_03_badge }}</span></h3>
|
||||
<p class="journey-step__desc">
|
||||
{% if lang == 'de' %}
|
||||
Kontakte zu Banken und Investoren herstellen. Dein Finanzplan wird zum Businesscase.
|
||||
{% else %}
|
||||
Connect with banks and investors. Your planner becomes your business case.
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="journey-step__desc">{{ t.landing_journey_03_desc }}</p>
|
||||
</div>
|
||||
<div class="journey-step journey-step--active">
|
||||
<div class="journey-step__num">04</div>
|
||||
<h3 class="journey-step__title">{{ t.landing_journey_04 }}</h3>
|
||||
<p class="journey-step__desc">
|
||||
{% if lang == 'de' %}
|
||||
Über {{ total_suppliers }}+ Platz-Anbieter aus {{ total_countries }} Ländern durchsuchen. Passend zu deinen Anforderungen vermittelt.
|
||||
{% else %}
|
||||
Browse {{ total_suppliers }}+ court suppliers across {{ total_countries }} countries. Get matched to your specs.
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="journey-step__desc">{{ t.landing_journey_04_desc | tformat(total_suppliers=total_suppliers, total_countries=total_countries) }}</p>
|
||||
</div>
|
||||
<div class="journey-step journey-step--upcoming">
|
||||
<div class="journey-step__num">05</div>
|
||||
<h3 class="journey-step__title">{{ t.landing_journey_05 }} <span class="badge-soon">{{ t.landing_journey_05_badge }}</span></h3>
|
||||
<p class="journey-step__desc">
|
||||
{% if lang == 'de' %}
|
||||
Launch-Playbook, Performance-Benchmarks und Wachstumsanalysen für deinen Betrieb.
|
||||
{% else %}
|
||||
Launch playbook, performance benchmarks, and expansion analytics.
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="journey-step__desc">{{ t.landing_journey_05_desc }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -380,65 +343,29 @@
|
||||
<div class="grid-3">
|
||||
<div class="card border-l-4 border-l-electric">
|
||||
<h3 class="text-lg mb-2">🔧 {{ t.landing_feature_1_h3 }}</h3>
|
||||
<p class="text-sm text-slate-dark">
|
||||
{% if lang == 'de' %}
|
||||
Jede Annahme ist anpassbar: Platzbaukosten, Miete, Preisgestaltung, Auslastung, Finanzierungskonditionen, Exit-Szenarien. Nichts ist fest vorgegeben.
|
||||
{% else %}
|
||||
Every assumption is adjustable. Court costs, rent, pricing, utilization, financing terms, exit scenarios. Nothing is hard-coded.
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="text-sm text-slate-dark">{{ t.landing_feature_1_body }}</p>
|
||||
</div>
|
||||
<div class="card border-l-4 border-l-accent">
|
||||
<h3 class="text-lg mb-2">📋 {{ t.landing_feature_2_h3 }}</h3>
|
||||
<p class="text-sm text-slate-dark">
|
||||
{% if lang == 'de' %}
|
||||
Annahmen, Investition (CAPEX), Betriebsmodell, Cashflow, Renditen & Exit sowie Kennzahlen — jeder Tab mit interaktiven Diagrammen.
|
||||
{% else %}
|
||||
Assumptions, Investment (CAPEX), Operating Model, Cash Flow, Returns & Exit, and Key Metrics. Each with interactive charts.
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="text-sm text-slate-dark">{{ t.landing_feature_2_body }}</p>
|
||||
</div>
|
||||
<div class="card border-l-4 border-l-warning">
|
||||
<h3 class="text-lg mb-2">☀️ {{ t.landing_feature_3_h3 }}</h3>
|
||||
<p class="text-sm text-slate-dark">
|
||||
{% if lang == 'de' %}
|
||||
Innenhallenmodelle (Miete oder Neubau) und Außenanlagen mit Saisonalität. Szenarien direkt nebeneinander vergleichen.
|
||||
{% else %}
|
||||
Model indoor halls (rent or build) and outdoor courts with seasonality. Compare scenarios side by side.
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="text-sm text-slate-dark">{{ t.landing_feature_3_body }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid-3 mt-0">
|
||||
<div class="card border-l-4 border-l-danger">
|
||||
<h3 class="text-lg mb-2">📉 {{ t.landing_feature_4_h3 }}</h3>
|
||||
<p class="text-sm text-slate-dark">
|
||||
{% if lang == 'de' %}
|
||||
Sieh dir an, wie sich deine Renditen bei unterschiedlichen Auslastungsraten und Preisen verändern. Break-even-Punkt sofort ermitteln.
|
||||
{% else %}
|
||||
See how your returns change with different utilization rates and pricing. Find your break-even point instantly.
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="text-sm text-slate-dark">{{ t.landing_feature_4_body }}</p>
|
||||
</div>
|
||||
<div class="card border-l-4 border-l-electric">
|
||||
<h3 class="text-lg mb-2">🎯 {{ t.landing_feature_5_h3 }}</h3>
|
||||
<p class="text-sm text-slate-dark">
|
||||
{% if lang == 'de' %}
|
||||
IRR, MOIC, DSCR, Cash-on-Cash-Rendite, Break-even-Auslastung, RevPAH, Schuldenrendite — die Kennzahlen, die Banken und Investoren sehen möchten.
|
||||
{% else %}
|
||||
IRR, MOIC, DSCR, cash-on-cash yield, break-even utilization, RevPAH, debt yield. The metrics banks and investors want to see.
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="text-sm text-slate-dark">{{ t.landing_feature_5_body }}</p>
|
||||
</div>
|
||||
<div class="card border-l-4 border-l-accent">
|
||||
<h3 class="text-lg mb-2">💾 {{ t.landing_feature_6_h3 }}</h3>
|
||||
<p class="text-sm text-slate-dark">
|
||||
{% if lang == 'de' %}
|
||||
Unbegrenzte Szenarien speichern. Verschiedene Standorte, Platzzahlen und Finanzierungsstrukturen testen. Den optimalen Plan finden.
|
||||
{% else %}
|
||||
Save unlimited scenarios. Test different locations, court counts, financing structures. Find the optimal plan.
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="text-sm text-slate-dark">{{ t.landing_feature_6_body }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -446,46 +373,22 @@
|
||||
<!-- Supplier Matching -->
|
||||
<section class="py-12">
|
||||
<h2 class="text-2xl text-center mb-2">{{ t.landing_supplier_title }}</h2>
|
||||
<p class="text-center text-slate mb-8">
|
||||
{% if lang == 'de' %}
|
||||
{{ total_suppliers }}+ verifizierte Anbieter aus {{ total_countries }} Ländern. Hersteller, Baufirmen, Belaghersteller, Beleuchtung und mehr.
|
||||
{% else %}
|
||||
{{ total_suppliers }}+ verified suppliers across {{ total_countries }} countries. Manufacturers, builders, turf, lighting, and more.
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="text-center text-slate mb-8">{{ t.landing_supplier_sub | tformat(total_suppliers=total_suppliers, total_countries=total_countries) }}</p>
|
||||
<div class="match-grid">
|
||||
<div class="match-step">
|
||||
<div class="match-step__num">1</div>
|
||||
<h3>{{ t.landing_supplier_step_1_title }}</h3>
|
||||
<p>
|
||||
{% if lang == 'de' %}
|
||||
Nutze den Finanzplaner, um deine Platzzahl, dein Budget und deinen Zeitplan zu modellieren.
|
||||
{% else %}
|
||||
Use the financial planner to model your courts, budget, and timeline.
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>{{ t.landing_supplier_step_1_body }}</p>
|
||||
</div>
|
||||
<div class="match-step">
|
||||
<div class="match-step__num">2</div>
|
||||
<h3>{{ t.landing_supplier_step_2_title }}</h3>
|
||||
<p>
|
||||
{% if lang == 'de' %}
|
||||
Angebote anfordern — wir vermitteln dich anhand deiner Projektspezifikationen an passende Anbieter.
|
||||
{% else %}
|
||||
Request quotes and we match you with suppliers based on your project specs.
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>{{ t.landing_supplier_step_2_body }}</p>
|
||||
</div>
|
||||
<div class="match-step">
|
||||
<div class="match-step__num">3</div>
|
||||
<h3>{{ t.landing_supplier_step_3_title }}</h3>
|
||||
<p>
|
||||
{% if lang == 'de' %}
|
||||
Angebote von vermittelten Anbietern erhalten. Keine Kaltakquise erforderlich.
|
||||
{% else %}
|
||||
Receive proposals from matched suppliers. No cold outreach needed.
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>{{ t.landing_supplier_step_3_body }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center mt-8">
|
||||
@@ -499,53 +402,23 @@
|
||||
<div class="faq">
|
||||
<details>
|
||||
<summary>{{ t.landing_faq_q1 }}</summary>
|
||||
<p>
|
||||
{% if lang == 'de' %}
|
||||
Der Planer erstellt ein vollständiges Finanzmodell: CAPEX-Aufschlüsselung, monatliche Betriebskosten, Cashflow-Projektionen, Schuldendienst, IRR, MOIC, DSCR, Amortisationszeit, Break-even-Auslastung und Sensitivitätsanalyse. Es werden Indoor-/Outdoor-Anlagen, Miet- und Eigentumsmodelle sowie alle wesentlichen Kosten- und Erlösvariablen abgedeckt.
|
||||
{% else %}
|
||||
The planner produces a complete financial model: CAPEX breakdown, monthly operating costs, cash flow projections, debt service, IRR, MOIC, DSCR, payback period, break-even utilization, and sensitivity analysis. It covers indoor/outdoor, rent/buy, and all major cost and revenue variables.
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>{{ t.landing_faq_a1 }}</p>
|
||||
</details>
|
||||
<details>
|
||||
<summary>{{ t.landing_faq_q2 }}</summary>
|
||||
<p>
|
||||
{% if lang == 'de' %}
|
||||
Nein. Der Planer funktioniert sofort ohne Registrierung. Erstelle ein Konto, um Szenarien zu speichern, Konfigurationen zu vergleichen und PDF-Berichte zu exportieren.
|
||||
{% else %}
|
||||
No. The planner works instantly with no signup. Create an account to save scenarios, compare configurations, and export PDF reports.
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>{{ t.landing_faq_a2 }}</p>
|
||||
</details>
|
||||
<details>
|
||||
<summary>{{ t.landing_faq_q3 }}</summary>
|
||||
<p>
|
||||
{% if lang == 'de' %}
|
||||
Wenn du über den Planer Angebote anforderst, teilen wir deine Projektdetails (Anlagentyp, Platzzahl, Glas, Beleuchtung, Land, Budget, Zeitplan) mit passenden Anbietern aus unserem Verzeichnis. Diese kontaktieren dich direkt mit ihren Angeboten.
|
||||
{% else %}
|
||||
When you request quotes through the planner, we share your project details (venue type, court count, glass, lighting, country, budget, timeline) with relevant suppliers from our directory. They contact you directly with proposals.
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>{{ t.landing_faq_a3 }}</p>
|
||||
</details>
|
||||
<details>
|
||||
<summary>{{ t.landing_faq_q4 }}</summary>
|
||||
<p>
|
||||
{% if lang == 'de' %}
|
||||
Das Durchsuchen des Verzeichnisses ist für alle kostenlos. Anbieter erhalten standardmäßig einen Basiseintrag. Kostenpflichtige Pläne (Basic ab 39 €/Monat, Growth ab 199 €/Monat, Pro ab 499 €/Monat) schalten Anfrageformulare, vollständige Beschreibungen, Logos, verifizierte Badges und Prioritätsplatzierung frei.
|
||||
{% else %}
|
||||
Browsing the directory is free for everyone. Suppliers have a basic listing by default. Paid plans (Basic at €39/mo, Growth at €199/mo, Pro at €499/mo) unlock enquiry forms, full descriptions, logos, verified badges, and priority placement.
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>{{ t.landing_faq_a4 }}</p>
|
||||
</details>
|
||||
<details>
|
||||
<summary>{{ t.landing_faq_q5 }}</summary>
|
||||
<p>
|
||||
{% if lang == 'de' %}
|
||||
Das Modell verwendet reale Standardwerte auf Basis europäischer Marktdaten. Jede Annahme ist anpassbar, sodass du deine lokalen Gegebenheiten abbilden kannst. Die Sensitivitätsanalyse zeigt, wie sich die Ergebnisse in verschiedenen Szenarien verändern, und hilft dir, die Bandbreite möglicher Ergebnisse zu verstehen.
|
||||
{% else %}
|
||||
The model uses real-world defaults based on European market data. Every assumption is adjustable so you can match your local conditions. The sensitivity analysis shows how results change across different scenarios, helping you understand the range of outcomes.
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>{{ t.landing_faq_a5 }}</p>
|
||||
</details>
|
||||
</div>
|
||||
</section>
|
||||
@@ -554,21 +427,8 @@
|
||||
<section class="py-12 max-w-3xl mx-auto">
|
||||
<h2 class="text-2xl mb-4">{{ t.landing_seo_title }}</h2>
|
||||
<div class="space-y-4 text-slate-dark leading-relaxed">
|
||||
{% if lang == 'de' %}
|
||||
<p>
|
||||
Padel ist der am schnellsten wachsende Sport in Europa — die Nachfrage nach Plätzen übersteigt das Angebot in Deutschland, Österreich, der Schweiz und darüber hinaus bei weitem. Eine Paddelhalle zu eröffnen kann eine attraktive Investition sein, aber die Zahlen müssen stimmen. Eine typische Innenhalle mit 6–8 Plätzen erfordert zwischen 300.000 € (Anmietung eines Bestandsgebäudes) und 2–3 Mio. € (Neubau), mit Amortisationszeiten von 3–5 Jahren für gut gelegene Anlagen.
|
||||
</p>
|
||||
<p>
|
||||
Die entscheidenden Faktoren für den Erfolg sind Standort (treibt die Auslastung), Baukosten (CAPEX), Miet- oder Grundstückskosten sowie die Preisstrategie. Unser Finanzplaner ermöglicht es dir, alle diese Variablen interaktiv zu modellieren und die Auswirkungen auf IRR, MOIC, Cashflow und Schuldendienstdeckungsgrad in Echtzeit zu sehen. Ob du als Unternehmer deine erste Anlage prüfst, als Immobilienentwickler Padel in ein Mixed-Use-Projekt integrierst oder als Investor eine bestehende Paddelhalle bewertest — Padelnomics gibt dir die finanzielle Klarheit für fundierte Entscheidungen.
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
Padel is the fastest-growing sport in Europe, with demand for courts far outstripping supply in Germany, the UK, Scandinavia, and beyond. Opening a padel hall can be a lucrative investment, but the numbers need to work. A typical indoor padel venue with 6-8 courts requires between €300K (renting an existing building) and €2-3M (building new), with payback periods of 3-5 years for well-located venues.
|
||||
</p>
|
||||
<p>
|
||||
The key variables that determine success are location (driving utilization), construction costs (CAPEX), rent or land costs, and pricing strategy. Our financial planner lets you model all of these variables interactively, seeing the impact on your IRR, MOIC, cash flow, and debt service coverage ratio in real time. Whether you're an entrepreneur exploring your first venue, a real estate developer adding padel to a mixed-use project, or an investor evaluating a padel hall acquisition, Padelnomics gives you the financial clarity to make informed decisions.
|
||||
</p>
|
||||
{% endif %}
|
||||
<p>{{ t.landing_seo_p1 }}</p>
|
||||
<p>{{ t.landing_seo_p2 }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -576,13 +436,7 @@
|
||||
<section style="padding: 2rem 0 4rem">
|
||||
<div class="cta-card">
|
||||
<h2>{{ t.landing_final_cta_h2 }}</h2>
|
||||
<p>
|
||||
{% if lang == 'de' %}
|
||||
Modelliere deine Investition und lass dich mit verifizierten Platz-Anbietern aus {{ total_countries }} Ländern zusammenbringen.
|
||||
{% else %}
|
||||
Model your investment, then get matched with verified court suppliers across {{ total_countries }} countries.
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>{{ t.landing_final_cta_sub | tformat(total_countries=total_countries) }}</p>
|
||||
<a href="{{ url_for('planner.index') }}" class="cta-card__btn">{{ t.landing_final_cta_btn }}</a>
|
||||
</div>
|
||||
</section>
|
||||
@@ -656,7 +510,6 @@
|
||||
update();
|
||||
})();
|
||||
</script>
|
||||
{% if lang == 'de' %}
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
@@ -664,7 +517,7 @@
|
||||
"name": "Padelnomics",
|
||||
"url": "{{ config.BASE_URL }}",
|
||||
"logo": "{{ url_for('static', filename='images/logo.png', _external=True) }}",
|
||||
"description": "Professionelle Planungsplattform für Padelplatz-Investitionen. Finanzplaner, Anbieterverzeichnis und Marktinformationen für Padel-Unternehmer."
|
||||
"description": "{{ t.landing_jsonld_org_desc }}"
|
||||
}
|
||||
</script>
|
||||
<script type="application/ld+json">
|
||||
@@ -674,105 +527,45 @@
|
||||
"mainEntity": [
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "Was berechnet der Planer?",
|
||||
"name": "{{ t.landing_faq_q1 }}",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "Der Planer erstellt ein vollständiges Finanzmodell: CAPEX-Aufschlüsselung, monatliche Betriebskosten, Cashflow-Projektionen, Schuldendienst, IRR, MOIC, DSCR, Amortisationszeit, Break-even-Auslastung und Sensitivitätsanalyse. Es werden Indoor-/Outdoor-Anlagen, Miet- und Eigentumsmodelle sowie alle wesentlichen Kosten- und Erlösvariablen abgedeckt."
|
||||
"text": "{{ t.landing_faq_a1 }}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "Muss ich mich registrieren?",
|
||||
"name": "{{ t.landing_faq_q2 }}",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "Nein. Der Planer funktioniert sofort ohne Registrierung. Erstelle ein Konto, um Szenarien zu speichern, Konfigurationen zu vergleichen und PDF-Berichte zu exportieren."
|
||||
"text": "{{ t.landing_faq_a2 }}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "Wie funktioniert die Anbieter-Vermittlung?",
|
||||
"name": "{{ t.landing_faq_q3 }}",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "Wenn du über den Planer Angebote anforderst, teilen wir deine Projektdetails (Anlagentyp, Platzzahl, Glas, Beleuchtung, Land, Budget, Zeitplan) mit passenden Anbietern aus unserem Verzeichnis. Diese kontaktieren dich direkt mit ihren Angeboten."
|
||||
"text": "{{ t.landing_faq_a3 }}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "Ist das Anbieterverzeichnis kostenlos?",
|
||||
"name": "{{ t.landing_faq_q4 }}",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "Das Durchsuchen des Verzeichnisses ist für alle kostenlos. Anbieter erhalten standardmäßig einen Basiseintrag. Kostenpflichtige Pläne schalten Anfrageformulare, vollständige Beschreibungen, Logos, verifizierte Badges und Prioritätsplatzierung frei."
|
||||
"text": "{{ t.landing_faq_a4 }}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "Wie genau sind die Finanzprojektionen?",
|
||||
"name": "{{ t.landing_faq_q5 }}",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "Das Modell verwendet reale Standardwerte auf Basis europäischer Marktdaten. Jede Annahme ist anpassbar, sodass du deine lokalen Gegebenheiten abbilden kannst. Die Sensitivitätsanalyse zeigt, wie sich die Ergebnisse in verschiedenen Szenarien verändern."
|
||||
"text": "{{ t.landing_faq_a5 }}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
{% else %}
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Organization",
|
||||
"name": "Padelnomics",
|
||||
"url": "{{ config.BASE_URL }}",
|
||||
"logo": "{{ url_for('static', filename='images/logo.png', _external=True) }}",
|
||||
"description": "Professional padel court investment planning platform. Financial planner, supplier directory, and market intelligence for padel entrepreneurs."
|
||||
}
|
||||
</script>
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "FAQPage",
|
||||
"mainEntity": [
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "What does the planner calculate?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "The planner produces a complete financial model: CAPEX breakdown, monthly operating costs, cash flow projections, debt service, IRR, MOIC, DSCR, payback period, break-even utilization, and sensitivity analysis. It covers indoor/outdoor, rent/buy, and all major cost and revenue variables."
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "Do I need to sign up?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "No. The planner works instantly with no signup. Create an account to save scenarios, compare configurations, and export PDF reports."
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "How does supplier matching work?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "When you request quotes through the planner, we share your project details (venue type, court count, glass, lighting, country, budget, timeline) with relevant suppliers from our directory. They contact you directly with proposals."
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "Is the supplier directory free?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "Browsing the directory is free for everyone. Suppliers have a basic listing by default. Paid plans unlock full descriptions, logos, verified badges, and priority placement."
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "How accurate are the financial projections?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "The model uses real-world defaults based on European market data. Every assumption is adjustable so you can match your local conditions. The sensitivity analysis shows how results change across different scenarios, helping you understand the range of outcomes."
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -18,6 +18,7 @@ from ..core import (
|
||||
get_paddle_price,
|
||||
waitlist_gate,
|
||||
)
|
||||
from ..i18n import get_translations
|
||||
|
||||
bp = Blueprint(
|
||||
"suppliers",
|
||||
@@ -40,23 +41,14 @@ PLAN_FEATURES = {
|
||||
"monthly_credits": 0,
|
||||
"paddle_key_monthly": "supplier_basic_monthly",
|
||||
"paddle_key_yearly": "supplier_basic_yearly",
|
||||
"features": [
|
||||
"Verified badge",
|
||||
"Company logo",
|
||||
"Full description & tagline",
|
||||
"Website & contact details shown",
|
||||
"Services offered checklist",
|
||||
"Social links (LinkedIn, Instagram, YouTube)",
|
||||
"Enquiry form on listing page",
|
||||
],
|
||||
"features_de": [
|
||||
"Verifiziert-Badge",
|
||||
"Firmenlogo",
|
||||
"Vollständige Beschreibung & Slogan",
|
||||
"Website & Kontaktdaten sichtbar",
|
||||
"Checkliste angebotener Leistungen",
|
||||
"Social-Links (LinkedIn, Instagram, YouTube)",
|
||||
"Kontaktformular auf der Listing-Seite",
|
||||
"feature_keys": [
|
||||
"plan_basic_f1",
|
||||
"plan_basic_f2",
|
||||
"plan_basic_f3",
|
||||
"plan_basic_f4",
|
||||
"plan_basic_f5",
|
||||
"plan_basic_f6",
|
||||
"plan_basic_f7",
|
||||
],
|
||||
},
|
||||
"supplier_growth": {
|
||||
@@ -67,17 +59,11 @@ PLAN_FEATURES = {
|
||||
"monthly_credits": 30,
|
||||
"paddle_key_monthly": "supplier_growth",
|
||||
"paddle_key_yearly": "supplier_growth_yearly",
|
||||
"features": [
|
||||
"Everything in Basic",
|
||||
"30 lead credits/month",
|
||||
"Lead feed access",
|
||||
"Priority over Basic listings",
|
||||
],
|
||||
"features_de": [
|
||||
"Alles aus Basic",
|
||||
"30 Lead-Credits/Monat",
|
||||
"Zugang zum Lead-Feed",
|
||||
"Priorität gegenüber Basic-Einträgen",
|
||||
"feature_keys": [
|
||||
"plan_growth_f1",
|
||||
"plan_growth_f2",
|
||||
"plan_growth_f3",
|
||||
"plan_growth_f4",
|
||||
],
|
||||
},
|
||||
"supplier_pro": {
|
||||
@@ -89,28 +75,21 @@ PLAN_FEATURES = {
|
||||
"paddle_key_monthly": "supplier_pro",
|
||||
"paddle_key_yearly": "supplier_pro_yearly",
|
||||
"includes": ["logo", "highlight", "verified"],
|
||||
"features": [
|
||||
"Everything in Growth",
|
||||
"100 lead credits/month",
|
||||
"Company logo displayed",
|
||||
"Highlighted card border",
|
||||
"Priority placement",
|
||||
],
|
||||
"features_de": [
|
||||
"Alles aus Growth",
|
||||
"100 Lead-Credits/Monat",
|
||||
"Firmenlogo angezeigt",
|
||||
"Hervorgehobener Kartenrahmen",
|
||||
"Bevorzugte Platzierung",
|
||||
"feature_keys": [
|
||||
"plan_pro_f1",
|
||||
"plan_pro_f2",
|
||||
"plan_pro_f3",
|
||||
"plan_pro_f4",
|
||||
"plan_pro_f5",
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
BOOST_OPTIONS = [
|
||||
{"key": "boost_logo", "type": "logo", "name": "Logo", "price": 29, "desc": "Display your company logo"},
|
||||
{"key": "boost_highlight", "type": "highlight", "name": "Highlight", "price": 39, "desc": "Blue highlighted card border"},
|
||||
{"key": "boost_verified", "type": "verified", "name": "Verified Badge", "price": 49, "desc": "Verified checkmark badge"},
|
||||
{"key": "boost_card_color", "type": "card_color", "name": "Custom Card Color", "price": 19, "desc": "Stand out with a custom border color on your directory listing"},
|
||||
{"key": "boost_logo", "type": "logo", "name_key": "sd_boost_logo_name", "price": 29, "desc_key": "sd_boost_logo_desc"},
|
||||
{"key": "boost_highlight", "type": "highlight", "name_key": "sd_boost_highlight_name", "price": 39, "desc_key": "sd_boost_highlight_desc"},
|
||||
{"key": "boost_verified", "type": "verified", "name_key": "sd_boost_verified_name", "price": 49, "desc_key": "sd_boost_verified_desc"},
|
||||
{"key": "boost_card_color", "type": "card_color", "name_key": "sd_boost_card_color_name", "price": 19, "desc_key": "sd_boost_card_color_desc"},
|
||||
]
|
||||
|
||||
CREDIT_PACK_OPTIONS = [
|
||||
@@ -158,15 +137,16 @@ def _supplier_required(f):
|
||||
|
||||
@wraps(f)
|
||||
async def decorated(*args, **kwargs):
|
||||
t = get_translations(g.get("lang") or "en")
|
||||
if not g.get("user"):
|
||||
await flash("Please sign in to continue.", "warning")
|
||||
await flash(t["sd_flash_signin"], "warning")
|
||||
return redirect(url_for("auth.login", next=request.path))
|
||||
supplier = await fetch_one(
|
||||
"SELECT * FROM suppliers WHERE claimed_by = ? AND tier IN ('basic', 'growth', 'pro')",
|
||||
(g.user["id"],),
|
||||
)
|
||||
if not supplier:
|
||||
await flash("You need an active supplier plan to access this page.", "warning")
|
||||
await flash(t["sd_flash_active_plan"], "warning")
|
||||
return redirect(url_for("suppliers.signup"))
|
||||
g.supplier = supplier
|
||||
return await f(*args, **kwargs)
|
||||
@@ -180,15 +160,16 @@ def _lead_tier_required(f):
|
||||
|
||||
@wraps(f)
|
||||
async def decorated(*args, **kwargs):
|
||||
t = get_translations(g.get("lang") or "en")
|
||||
if not g.get("user"):
|
||||
await flash("Please sign in to continue.", "warning")
|
||||
await flash(t["sd_flash_signin"], "warning")
|
||||
return redirect(url_for("auth.login", next=request.path))
|
||||
supplier = await fetch_one(
|
||||
"SELECT * FROM suppliers WHERE claimed_by = ? AND tier IN ('growth', 'pro')",
|
||||
(g.user["id"],),
|
||||
)
|
||||
if not supplier:
|
||||
await flash("Lead access requires a Growth or Pro plan.", "warning")
|
||||
await flash(t["sd_flash_lead_access"], "warning")
|
||||
return redirect(url_for("suppliers.dashboard"))
|
||||
g.supplier = supplier
|
||||
return await f(*args, **kwargs)
|
||||
@@ -239,7 +220,8 @@ async def signup_waitlist():
|
||||
plan = form.get("plan", "supplier_growth")
|
||||
|
||||
if not email or "@" not in email:
|
||||
await flash("Please enter a valid email address.", "error")
|
||||
t = get_translations(g.get("lang") or "en")
|
||||
await flash(t["sd_flash_valid_email"], "error")
|
||||
return redirect(url_for("suppliers.signup", plan=plan))
|
||||
|
||||
# Capture to DB with intent="supplier", but email confirmation uses plan name
|
||||
@@ -281,7 +263,7 @@ async def signup_step(step: int):
|
||||
included_boosts = plan_info.get("includes", [])
|
||||
|
||||
# Compute order summary for step 4
|
||||
order = _compute_order(accumulated, included_boosts)
|
||||
order = _compute_order(accumulated, included_boosts, get_translations(g.lang))
|
||||
|
||||
return await render_template(
|
||||
f"suppliers/partials/signup_step_{next_step}.html",
|
||||
@@ -295,7 +277,7 @@ async def signup_step(step: int):
|
||||
)
|
||||
|
||||
|
||||
def _compute_order(data: dict, included_boosts: list) -> dict:
|
||||
def _compute_order(data: dict, included_boosts: list, t: dict) -> dict:
|
||||
"""Compute order summary from accumulated wizard state."""
|
||||
plan = data.get("plan", "supplier_growth")
|
||||
plan_info = PLAN_FEATURES.get(plan, PLAN_FEATURES["supplier_growth"])
|
||||
@@ -304,11 +286,11 @@ def _compute_order(data: dict, included_boosts: list) -> dict:
|
||||
if period == "yearly":
|
||||
plan_price = plan_info["yearly_price"]
|
||||
plan_price_display = plan_info["yearly_monthly_equivalent"]
|
||||
billing_label = f"billed annually at €{plan_price}/yr"
|
||||
billing_label = t["sd_billing_yearly"].format(price=plan_price)
|
||||
else:
|
||||
plan_price = plan_info["monthly_price"]
|
||||
plan_price_display = plan_info["monthly_price"]
|
||||
billing_label = "billed monthly"
|
||||
billing_label = t["sd_billing_monthly"]
|
||||
|
||||
one_time = 0
|
||||
selected_boosts = data.get("boosts", [])
|
||||
@@ -447,7 +429,8 @@ async def claim(slug: str):
|
||||
"SELECT * FROM suppliers WHERE slug = ? AND claimed_by IS NULL", (slug,)
|
||||
)
|
||||
if not supplier:
|
||||
await flash("This listing has already been claimed or does not exist.", "warning")
|
||||
t = get_translations(g.get("lang") or "en")
|
||||
await flash(t["sd_flash_claim_error"], "warning")
|
||||
return redirect(url_for("directory.index"))
|
||||
return redirect(url_for("suppliers.signup", claim=slug))
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Supplier Dashboard - {{ config.APP_NAME }}{% endblock %}
|
||||
{% block title %}{{ t.sd_page_title }} - {{ config.APP_NAME }}{% endblock %}
|
||||
{% block paddle %}{% include "_paddle.html" %}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
@@ -67,7 +67,7 @@
|
||||
hx-push-url="{{ url_for('suppliers.dashboard', tab='overview') }}"
|
||||
class="{% if active_tab == 'overview' %}active{% endif %}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 0 1 3 19.875v-6.75ZM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625ZM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z"/></svg>
|
||||
Overview
|
||||
{{ t.sd_nav_overview }}
|
||||
</a>
|
||||
{% if supplier.tier in ('growth', 'pro') %}
|
||||
<a href="{{ url_for('suppliers.dashboard', tab='leads') }}"
|
||||
@@ -76,7 +76,7 @@
|
||||
hx-push-url="{{ url_for('suppliers.dashboard', tab='leads') }}"
|
||||
class="{% if active_tab == 'leads' %}active{% endif %}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 13.5h3.86a2.25 2.25 0 0 1 2.012 1.244l.256.512a2.25 2.25 0 0 0 2.013 1.244h2.239a2.25 2.25 0 0 0 2.013-1.244l.256-.512a2.25 2.25 0 0 1 2.013-1.244h3.859M12 3v8.25m0 0-3-3m3 3 3-3"/></svg>
|
||||
Lead Feed
|
||||
{{ t.sd_nav_leads }}
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('suppliers.dashboard', tab='listing') }}"
|
||||
@@ -85,7 +85,7 @@
|
||||
hx-push-url="{{ url_for('suppliers.dashboard', tab='listing') }}"
|
||||
class="{% if active_tab == 'listing' %}active{% endif %}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 21h19.5M3.75 3v18m4.5-18v18M12 3v18m4.5-18v18m4.5-18v18M6 6.75h.008v.008H6V6.75Zm0 3h.008v.008H6V9.75Zm0 3h.008v.008H6v-.008Zm4.5-6h.008v.008H10.5V6.75Zm0 3h.008v.008H10.5V9.75Zm0 3h.008v.008H10.5v-.008Z"/></svg>
|
||||
My Listing
|
||||
{{ t.sd_nav_listing }}
|
||||
</a>
|
||||
{% if supplier.tier in ('growth', 'pro') %}
|
||||
<a href="{{ url_for('suppliers.dashboard', tab='boosts') }}"
|
||||
@@ -94,23 +94,23 @@
|
||||
hx-push-url="{{ url_for('suppliers.dashboard', tab='boosts') }}"
|
||||
class="{% if active_tab == 'boosts' %}active{% endif %}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M15.59 14.37a6 6 0 0 1-5.84 7.38v-4.8m5.84-2.58a14.98 14.98 0 0 0 6.16-12.12A14.98 14.98 0 0 0 9.631 8.41m5.96 5.96a14.926 14.926 0 0 1-5.841 2.58m-.119-8.54a6 6 0 0 0-7.381 5.84h4.8m2.581-5.84a14.927 14.927 0 0 0-2.58 5.84m2.699 2.7c-.103.021-.207.041-.311.06a15.09 15.09 0 0 1-2.448-2.448 14.9 14.9 0 0 1 .06-.312m-2.24 2.39a4.493 4.493 0 0 0-1.757 4.306 4.493 4.493 0 0 0 4.306-1.758M16.5 9a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Z"/></svg>
|
||||
Boost & Upsells
|
||||
{{ t.sd_nav_boosts }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
|
||||
{% if supplier.tier == 'basic' %}
|
||||
<div style="margin:0.75rem 1.25rem;padding:10px 12px;background:#EFF6FF;border:1px solid #BFDBFE;border-radius:8px;font-size:0.75rem;color:#1D4ED8">
|
||||
<strong>Basic plan</strong> — directory listing + enquiry form.
|
||||
<strong>{{ t.sd_basic_plan_label }}</strong> — {{ t.sd_basic_plan_desc }}
|
||||
<a href="{{ url_for('suppliers.signup') }}" style="display:block;font-weight:600;margin-top:4px;color:#1D4ED8">
|
||||
Upgrade to Growth for lead access →
|
||||
{{ t.sd_upgrade_growth }} →
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="dash-sidebar__footer">
|
||||
<div class="dash-sidebar__credits" id="sidebar-credits">
|
||||
{{ supplier.credit_balance }} credits
|
||||
{{ supplier.credit_balance }} {{ t.sd_credits }}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -129,7 +129,7 @@
|
||||
hx-get="{% if active_tab == 'leads' and supplier.tier in ('growth', 'pro') %}{{ url_for('suppliers.dashboard_leads') }}{% elif active_tab == 'listing' %}{{ url_for('suppliers.dashboard_listing') }}{% elif active_tab == 'boosts' and supplier.tier in ('growth', 'pro') %}{{ url_for('suppliers.dashboard_boosts') }}{% else %}{{ url_for('suppliers.dashboard_overview') }}{% endif %}"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML">
|
||||
<div style="text-align:center;padding:3rem;color:#94A3B8">Loading...</div>
|
||||
<div style="text-align:center;padding:3rem;color:#94A3B8">{{ t.sd_loading }}</div>
|
||||
</main>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Lead Feed - {{ config.APP_NAME }}{% endblock %}
|
||||
{% block title %}{{ t.sd_lf_page_title }} - {{ config.APP_NAME }}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<style>
|
||||
@@ -49,23 +49,23 @@
|
||||
<main class="container-page py-12">
|
||||
<div class="lf-header">
|
||||
<div>
|
||||
<h1 class="text-2xl">Lead Feed</h1>
|
||||
<p class="text-sm text-slate">Browse and unlock qualified padel project leads.</p>
|
||||
<h1 class="text-2xl">{{ t.sd_lf_h1 }}</h1>
|
||||
<p class="text-sm text-slate">{{ t.sd_lf_subtitle }}</p>
|
||||
</div>
|
||||
<div class="lf-balance">
|
||||
{{ supplier.credit_balance }} credits available
|
||||
{{ supplier.credit_balance }} {{ t.sd_lf_credits_available }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lf-filters">
|
||||
<select onchange="window.location='?country='+this.value+'&heat={{ current_heat }}'" class="form-input" style="min-width:120px">
|
||||
<option value="">All countries</option>
|
||||
<option value="">{{ t.sd_lf_all_countries }}</option>
|
||||
{% for c in countries %}
|
||||
<option value="{{ c }}" {% if c == current_country %}selected{% endif %}>{{ c }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<select onchange="window.location='?country={{ current_country }}&heat='+this.value" class="form-input" style="min-width:100px">
|
||||
<option value="">All heat</option>
|
||||
<option value="">{{ t.sd_lf_all_heat }}</option>
|
||||
<option value="hot" {% if current_heat == 'hot' %}selected{% endif %}>HOT</option>
|
||||
<option value="warm" {% if current_heat == 'warm' %}selected{% endif %}>WARM</option>
|
||||
<option value="cool" {% if current_heat == 'cool' %}selected{% endif %}>COOL</option>
|
||||
@@ -86,8 +86,8 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card text-center" style="padding:3rem">
|
||||
<h3 style="color:#64748B">No leads match your filters</h3>
|
||||
<p style="color:#94A3B8;font-size:0.875rem">Try adjusting your country or heat filters, or check back later for new leads.</p>
|
||||
<h3 style="color:#64748B">{{ t.sd_lf_no_match }}</h3>
|
||||
<p style="color:#94A3B8;font-size:0.875rem">{{ t.sd_lf_no_match_hint }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</main>
|
||||
|
||||
@@ -56,57 +56,57 @@
|
||||
<div style="max-width:720px">
|
||||
<!-- Current Plan -->
|
||||
<div class="bst-section">
|
||||
<h3>Current Plan</h3>
|
||||
<h3>{{ t.sd_bst_current_plan }}</h3>
|
||||
<div class="bst-plan">
|
||||
<div>
|
||||
<div class="bst-plan__name">{{ plan_info.name }}</div>
|
||||
<div class="bst-plan__credits">{{ supplier.monthly_credits }} credits/month</div>
|
||||
<div class="bst-plan__credits">{{ supplier.monthly_credits }} {{ t.sd_bst_credits_month }}</div>
|
||||
</div>
|
||||
<div class="bst-plan__price">€{{ plan_info.price }} <span>/mo</span></div>
|
||||
<div class="bst-plan__price">€{{ plan_info.price }} <span>{{ t.sd_bst_per_mo }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active Boosts -->
|
||||
<div class="bst-section">
|
||||
<h3>Active Boosts</h3>
|
||||
<h3>{{ t.sd_bst_active_boosts }}</h3>
|
||||
{% if active_boosts %}
|
||||
{% for boost in active_boosts %}
|
||||
<div class="bst-boost-card bst-boost-card--active">
|
||||
<div>
|
||||
<div class="bst-boost__name">{{ boost.boost_type | replace('_', ' ') | title }}</div>
|
||||
{% if boost.expires_at %}
|
||||
<div class="bst-boost__desc">Expires {{ boost.expires_at[:10] }}</div>
|
||||
<div class="bst-boost__desc">{{ t.sd_bst_expires }} {{ boost.expires_at[:10] }}</div>
|
||||
{% else %}
|
||||
<div class="bst-boost__desc">Active subscription</div>
|
||||
<div class="bst-boost__desc">{{ t.sd_bst_active_subscription }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<span class="bst-boost__status">Active</span>
|
||||
<span class="bst-boost__status">{{ t.sd_bst_active }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p style="color:#94A3B8;font-size:0.8125rem;text-align:center;padding:0.5rem 0">No active boosts</p>
|
||||
<p style="color:#94A3B8;font-size:0.8125rem;text-align:center;padding:0.5rem 0">{{ t.sd_bst_no_active_boosts }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Available Boosts -->
|
||||
<div class="bst-section">
|
||||
<h3>Available Boosts</h3>
|
||||
<h3>{{ t.sd_bst_available_boosts }}</h3>
|
||||
{% for b in boost_options %}
|
||||
{% if b.type not in active_boost_types %}
|
||||
<div class="bst-boost-card">
|
||||
<div>
|
||||
<div class="bst-boost__name">{{ b.name }}</div>
|
||||
<div class="bst-boost__desc">{{ b.desc }}</div>
|
||||
<div class="bst-boost__name">{{ t[b.name_key] }}</div>
|
||||
<div class="bst-boost__desc">{{ t[b.desc_key] }}</div>
|
||||
</div>
|
||||
<div style="text-align:right">
|
||||
<div class="bst-boost__price">€{{ b.price }}/mo</div>
|
||||
{% if price_ids.get(b.key) %}
|
||||
<button type="button" class="bst-buy-btn"
|
||||
onclick="Paddle.Checkout.open({items:[{priceId:'{{ price_ids[b.key] }}',quantity:1}],customData:{supplier_id:'{{ supplier.id }}'}})">
|
||||
Activate
|
||||
{{ t.sd_bst_activate }}
|
||||
</button>
|
||||
{% else %}
|
||||
<span style="font-size:0.6875rem;color:#94A3B8">Not configured</span>
|
||||
<span style="font-size:0.6875rem;color:#94A3B8">{{ t.sd_bst_not_configured }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -116,20 +116,20 @@
|
||||
|
||||
<!-- Credit Packs -->
|
||||
<div class="bst-section">
|
||||
<h3>Buy Credit Packs</h3>
|
||||
<h3>{{ t.sd_bst_buy_credits }}</h3>
|
||||
<div class="bst-credits-grid">
|
||||
{% for cp in credit_packs %}
|
||||
<div class="bst-credit-card">
|
||||
<div class="bst-credit-card__amount">{{ cp.amount }}</div>
|
||||
<div class="bst-credit-card__label">credits</div>
|
||||
<div class="bst-credit-card__label">{{ t.sd_bst_credits }}</div>
|
||||
<div class="bst-credit-card__price">€{{ cp.price }}</div>
|
||||
{% if price_ids.get(cp.key) %}
|
||||
<button type="button" class="bst-buy-btn"
|
||||
onclick="Paddle.Checkout.open({items:[{priceId:'{{ price_ids[cp.key] }}',quantity:1}],customData:{supplier_id:'{{ supplier.id }}'}})">
|
||||
Buy
|
||||
{{ t.sd_bst_buy }}
|
||||
</button>
|
||||
{% else %}
|
||||
<span style="font-size:0.6875rem;color:#94A3B8">Not configured</span>
|
||||
<span style="font-size:0.6875rem;color:#94A3B8">{{ t.sd_bst_not_configured }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
@@ -140,21 +140,21 @@
|
||||
<!-- Summary Sidebar -->
|
||||
<div class="bst-sidebar">
|
||||
<div class="bst-summary">
|
||||
<h3>Summary</h3>
|
||||
<h3>{{ t.sd_bst_summary }}</h3>
|
||||
<div class="bst-summary__row">
|
||||
<span>{{ plan_info.name }} plan</span>
|
||||
<span>{{ plan_info.name }} {{ t.sd_bst_plan_suffix }}</span>
|
||||
<span>€{{ plan_info.price }}/mo</span>
|
||||
</div>
|
||||
{% for boost in active_boosts %}
|
||||
{% if not boost.expires_at %}
|
||||
<div class="bst-summary__row">
|
||||
<span>{{ boost.boost_type | replace('_', ' ') | title }}</span>
|
||||
<span>subscription</span>
|
||||
<span>{{ t.sd_bst_subscription }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<div class="bst-summary__row bst-summary__total">
|
||||
<span>Credits Balance</span>
|
||||
<span>{{ t.sd_bst_credits_balance }}</span>
|
||||
<span style="color:#1D4ED8">{{ supplier.credit_balance }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -32,17 +32,17 @@
|
||||
</style>
|
||||
|
||||
<div class="dl-top">
|
||||
<h2 style="font-size:1.25rem;margin:0">Lead Feed</h2>
|
||||
<h2 style="font-size:1.25rem;margin:0">{{ t.sd_leads_h2 }}</h2>
|
||||
<div class="dl-balance" id="dl-credit-balance">
|
||||
{{ supplier.credit_balance }} credits
|
||||
{{ supplier.credit_balance }} {{ t.sd_leads_credits }}
|
||||
<a href="{{ url_for('suppliers.dashboard', tab='boosts') }}"
|
||||
hx-get="{{ url_for('suppliers.dashboard_boosts') }}"
|
||||
hx-target="#dashboard-content"
|
||||
hx-push-url="{{ url_for('suppliers.dashboard', tab='boosts') }}">Buy More</a>
|
||||
hx-push-url="{{ url_for('suppliers.dashboard', tab='boosts') }}">{{ t.sd_leads_buy_more }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="search" name="q" class="dl-search" placeholder="Search leads by country, type, details..."
|
||||
<input type="search" name="q" class="dl-search" placeholder="{{ t.sd_leads_search_placeholder }}"
|
||||
value="{{ current_q if current_q is defined else '' }}"
|
||||
hx-get="{{ url_for('suppliers.dashboard_leads') }}"
|
||||
hx-trigger="input changed delay:300ms"
|
||||
@@ -54,13 +54,13 @@
|
||||
hx-push-url="false">
|
||||
<!-- Heat filters -->
|
||||
<a class="dl-pill {% if not current_heat %}dl-pill--active{% endif %}"
|
||||
hx-get="{{ url_for('suppliers.dashboard_leads', country=current_country, timeline=current_timeline) }}">All</a>
|
||||
hx-get="{{ url_for('suppliers.dashboard_leads', country=current_country, timeline=current_timeline) }}">{{ t.sd_leads_filter_all }}</a>
|
||||
<a class="dl-pill {% if current_heat == 'hot' %}dl-pill--active{% endif %}"
|
||||
hx-get="{{ url_for('suppliers.dashboard_leads', heat='hot', country=current_country, timeline=current_timeline) }}">Hot</a>
|
||||
hx-get="{{ url_for('suppliers.dashboard_leads', heat='hot', country=current_country, timeline=current_timeline) }}">{{ t.sd_leads_filter_hot }}</a>
|
||||
<a class="dl-pill {% if current_heat == 'warm' %}dl-pill--active{% endif %}"
|
||||
hx-get="{{ url_for('suppliers.dashboard_leads', heat='warm', country=current_country, timeline=current_timeline) }}">Warm</a>
|
||||
hx-get="{{ url_for('suppliers.dashboard_leads', heat='warm', country=current_country, timeline=current_timeline) }}">{{ t.sd_leads_filter_warm }}</a>
|
||||
<a class="dl-pill {% if current_heat == 'cool' %}dl-pill--active{% endif %}"
|
||||
hx-get="{{ url_for('suppliers.dashboard_leads', heat='cool', country=current_country, timeline=current_timeline) }}">Cool</a>
|
||||
hx-get="{{ url_for('suppliers.dashboard_leads', heat='cool', country=current_country, timeline=current_timeline) }}">{{ t.sd_leads_filter_cool }}</a>
|
||||
|
||||
<div class="dl-sep"></div>
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
hx-target="#dashboard-content"
|
||||
hx-include="[name='heat'],[name='timeline']"
|
||||
name="country">
|
||||
<option value="">All countries</option>
|
||||
<option value="">{{ t.sd_leads_filter_countries }}</option>
|
||||
{% for c in countries %}
|
||||
<option value="{{ c }}" {% if c == current_country %}selected{% endif %}>{{ c }}</option>
|
||||
{% endfor %}
|
||||
@@ -82,13 +82,13 @@
|
||||
|
||||
<!-- Timeline filters -->
|
||||
<a class="dl-pill {% if not current_timeline %}dl-pill--active{% endif %}"
|
||||
hx-get="{{ url_for('suppliers.dashboard_leads', heat=current_heat, country=current_country) }}">Any</a>
|
||||
hx-get="{{ url_for('suppliers.dashboard_leads', heat=current_heat, country=current_country) }}">{{ t.sd_leads_filter_any }}</a>
|
||||
<a class="dl-pill {% if current_timeline == 'asap' %}dl-pill--active{% endif %}"
|
||||
hx-get="{{ url_for('suppliers.dashboard_leads', heat=current_heat, country=current_country, timeline='asap') }}">ASAP</a>
|
||||
hx-get="{{ url_for('suppliers.dashboard_leads', heat=current_heat, country=current_country, timeline='asap') }}">{{ t.sd_leads_filter_asap }}</a>
|
||||
<a class="dl-pill {% if current_timeline == '3_6_months' %}dl-pill--active{% endif %}"
|
||||
hx-get="{{ url_for('suppliers.dashboard_leads', heat=current_heat, country=current_country, timeline='3_6_months') }}">3-6mo</a>
|
||||
hx-get="{{ url_for('suppliers.dashboard_leads', heat=current_heat, country=current_country, timeline='3_6_months') }}">{{ t.sd_leads_filter_3_6mo }}</a>
|
||||
<a class="dl-pill {% if current_timeline == '6_12_months' %}dl-pill--active{% endif %}"
|
||||
hx-get="{{ url_for('suppliers.dashboard_leads', heat=current_heat, country=current_country, timeline='6_12_months') }}">6-12mo</a>
|
||||
hx-get="{{ url_for('suppliers.dashboard_leads', heat=current_heat, country=current_country, timeline='6_12_months') }}">{{ t.sd_leads_filter_6_12mo }}</a>
|
||||
</div>
|
||||
|
||||
{% if leads %}
|
||||
@@ -103,27 +103,27 @@
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.5rem">
|
||||
<span class="lf-card__heat lf-card__heat--{{ lead.heat_score or 'cool' }}">{{ (lead.heat_score or 'cool') | upper }}</span>
|
||||
{% if lead.country in service_area %}
|
||||
<span class="dl-badge-region">Your region</span>
|
||||
<span class="dl-badge-region">{{ t.sd_leads_region_badge }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<dl class="lf-card__meta">
|
||||
<dt>Facility</dt><dd>{{ lead.facility_type or '-' }}</dd>
|
||||
<dt>Courts</dt><dd>{{ lead.court_count or '-' }}</dd>
|
||||
<dt>Country</dt><dd>{{ lead.country or '-' }}</dd>
|
||||
<dt>Timeline</dt><dd>{{ lead.timeline or '-' }}</dd>
|
||||
<dt>Budget</dt><dd>{% if lead.budget_estimate %}€{{ lead.budget_estimate }}{% else %}-{% endif %}</dd>
|
||||
<dt>{{ t.sd_leads_facility }}</dt><dd>{{ lead.facility_type or '-' }}</dd>
|
||||
<dt>{{ t.sd_leads_courts }}</dt><dd>{{ lead.court_count or '-' }}</dd>
|
||||
<dt>{{ t.sd_leads_country }}</dt><dd>{{ lead.country or '-' }}</dd>
|
||||
<dt>{{ t.sd_leads_timeline }}</dt><dd>{{ lead.timeline or '-' }}</dd>
|
||||
<dt>{{ t.sd_leads_budget }}</dt><dd>{% if lead.budget_estimate %}€{{ lead.budget_estimate }}{% else %}-{% endif %}</dd>
|
||||
</dl>
|
||||
{# Bidder count messaging #}
|
||||
{% if lead.bidder_count == 0 %}
|
||||
<div class="dl-bidders dl-bidders--first">No other suppliers yet — be first!</div>
|
||||
<div class="dl-bidders dl-bidders--first">{{ t.sd_leads_be_first }}</div>
|
||||
{% else %}
|
||||
<div class="dl-bidders dl-bidders--many">{{ lead.bidder_count }} supplier{{ 's' if lead.bidder_count != 1 }} already unlocked</div>
|
||||
<div class="dl-bidders dl-bidders--many">{{ lead.bidder_count }} {{ t.sd_leads_already_unlocked }}</div>
|
||||
{% endif %}
|
||||
<div class="lf-card__foot">
|
||||
<div class="lf-card__cost"><strong>{{ lead.credit_cost or '?' }}</strong> credits to unlock</div>
|
||||
<div class="lf-card__cost"><strong>{{ lead.credit_cost or '?' }}</strong> {{ t.sd_leads_credits_to_unlock }}</div>
|
||||
<form hx-post="{{ url_for('suppliers.unlock_lead', lead_id=lead.id) }}" hx-target="#lead-card-{{ lead.id }}" hx-swap="innerHTML">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="lf-unlock-btn">Unlock</button>
|
||||
<button type="submit" class="lf-unlock-btn">{{ t.sd_leads_unlock }}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -133,8 +133,8 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card text-center" style="padding:3rem">
|
||||
<h3 style="color:#64748B">No leads match your filters</h3>
|
||||
<p style="color:#94A3B8;font-size:0.875rem">Try adjusting your filters, or check back later for new leads.</p>
|
||||
<h3 style="color:#64748B">{{ t.sd_leads_no_match }}</h3>
|
||||
<p style="color:#94A3B8;font-size:0.875rem">{{ t.sd_leads_no_match_hint }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -43,12 +43,12 @@
|
||||
</style>
|
||||
|
||||
{% if saved is defined and saved %}
|
||||
<div class="lst-saved">Listing saved successfully.</div>
|
||||
<div class="lst-saved">{{ t.sd_lst_saved }}</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Listing Preview -->
|
||||
<div class="lst-preview">
|
||||
<h3>Your Directory Card Preview</h3>
|
||||
<h3>{{ t.sd_lst_preview_title }}</h3>
|
||||
<div id="lst-preview">
|
||||
{% include "suppliers/partials/dashboard_listing_preview.html" %}
|
||||
</div>
|
||||
@@ -56,53 +56,53 @@
|
||||
|
||||
<!-- Edit Form -->
|
||||
<div class="lst-form">
|
||||
<h3>Edit Company Info</h3>
|
||||
<h3>{{ t.sd_lst_edit_title }}</h3>
|
||||
<form id="lst-edit-form" hx-post="{{ url_for('suppliers.dashboard_listing_save') }}" hx-target="#dashboard-content" hx-swap="innerHTML" hx-encoding="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<div class="lst-row">
|
||||
<div>
|
||||
<label class="lst-label">Company Name</label>
|
||||
<label class="lst-label">{{ t.sd_lst_company_name }}</label>
|
||||
<input type="text" name="name" value="{{ supplier.name }}" class="lst-input"
|
||||
hx-get="{{ url_for('suppliers.dashboard_listing_preview') }}" hx-trigger="input changed delay:500ms"
|
||||
hx-target="#lst-preview" hx-include="#lst-edit-form">
|
||||
</div>
|
||||
<div>
|
||||
<label class="lst-label">Tagline</label>
|
||||
<input type="text" name="tagline" value="{{ supplier.tagline or '' }}" class="lst-input" placeholder="One-liner for search results"
|
||||
<label class="lst-label">{{ t.sd_lst_tagline }}</label>
|
||||
<input type="text" name="tagline" value="{{ supplier.tagline or '' }}" class="lst-input" placeholder="{{ t.sd_lst_tagline_placeholder }}"
|
||||
hx-get="{{ url_for('suppliers.dashboard_listing_preview') }}" hx-trigger="input changed delay:500ms"
|
||||
hx-target="#lst-preview" hx-include="#lst-edit-form">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lst-full">
|
||||
<label class="lst-label">Short Description</label>
|
||||
<label class="lst-label">{{ t.sd_lst_short_desc }}</label>
|
||||
<textarea name="short_description" class="lst-input lst-textarea"
|
||||
hx-get="{{ url_for('suppliers.dashboard_listing_preview') }}" hx-trigger="input changed delay:500ms"
|
||||
hx-target="#lst-preview" hx-include="#lst-edit-form">{{ supplier.short_description or '' }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="lst-full">
|
||||
<label class="lst-label">Full Description</label>
|
||||
<label class="lst-label">{{ t.sd_lst_full_desc }}</label>
|
||||
<textarea name="long_description" class="lst-input lst-textarea" style="min-height:120px">{{ supplier.long_description or '' }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="lst-row">
|
||||
<div>
|
||||
<label class="lst-label">Website</label>
|
||||
<label class="lst-label">{{ t.sd_lst_website }}</label>
|
||||
<input type="url" name="website" value="{{ supplier.website or '' }}" class="lst-input" placeholder="https://..."
|
||||
hx-get="{{ url_for('suppliers.dashboard_listing_preview') }}" hx-trigger="input changed delay:500ms"
|
||||
hx-target="#lst-preview" hx-include="#lst-edit-form">
|
||||
</div>
|
||||
<div>
|
||||
<label class="lst-label">Logo</label>
|
||||
<label class="lst-label">{{ t.sd_lst_logo }}</label>
|
||||
<input type="file" name="logo_file" accept="image/*" class="lst-input" style="padding:6px 8px">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lst-full">
|
||||
<label class="lst-label">Cover Photo
|
||||
<span style="font-weight:400;color:#94A3B8"> — 16:9, min 640px wide. Shown in directory search results.</span>
|
||||
<label class="lst-label">{{ t.sd_lst_cover_photo }}
|
||||
<span style="font-weight:400;color:#94A3B8">{{ t.sd_lst_cover_hint }}</span>
|
||||
</label>
|
||||
{% if supplier.cover_image %}
|
||||
<img src="{{ supplier.cover_image }}" alt="Current cover" style="width:100%;aspect-ratio:16/9;object-fit:cover;border-radius:8px;margin-bottom:6px;border:1px solid #E2E8F0">
|
||||
@@ -112,33 +112,33 @@
|
||||
|
||||
<div class="lst-row">
|
||||
<div>
|
||||
<label class="lst-label">Contact Name</label>
|
||||
<label class="lst-label">{{ t.sd_lst_contact_name }}</label>
|
||||
<input type="text" name="contact_name" value="{{ supplier.contact_name or '' }}" class="lst-input">
|
||||
</div>
|
||||
<div>
|
||||
<label class="lst-label">Contact Email</label>
|
||||
<label class="lst-label">{{ t.sd_lst_contact_email }}</label>
|
||||
<input type="email" name="contact_email" value="{{ supplier.contact_email or '' }}" class="lst-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lst-row">
|
||||
<div>
|
||||
<label class="lst-label">Contact Phone</label>
|
||||
<label class="lst-label">{{ t.sd_lst_contact_phone }}</label>
|
||||
<input type="tel" name="contact_phone" value="{{ supplier.contact_phone or '' }}" class="lst-input">
|
||||
</div>
|
||||
<div>
|
||||
<label class="lst-label">Years in Business</label>
|
||||
<label class="lst-label">{{ t.sd_lst_years_in_business }}</label>
|
||||
<input type="number" name="years_in_business" value="{{ supplier.years_in_business or '' }}" class="lst-input" min="0">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lst-full">
|
||||
<label class="lst-label">Project Count</label>
|
||||
<label class="lst-label">{{ t.sd_lst_project_count }}</label>
|
||||
<input type="number" name="project_count" value="{{ supplier.project_count or '' }}" class="lst-input" min="0" style="max-width:200px">
|
||||
</div>
|
||||
|
||||
<div class="lst-full">
|
||||
<label class="lst-label">Service Categories</label>
|
||||
<label class="lst-label">{{ t.sd_lst_service_categories }}</label>
|
||||
<div class="lst-pills">
|
||||
{% set current_cats = (supplier.service_categories or '').split(',') %}
|
||||
{% for cat in service_categories %}
|
||||
@@ -151,7 +151,7 @@
|
||||
</div>
|
||||
|
||||
<div class="lst-full" style="margin-top:0.75rem">
|
||||
<label class="lst-label">Service Area (Countries)</label>
|
||||
<label class="lst-label">{{ t.sd_lst_service_area }}</label>
|
||||
<div class="lst-pills">
|
||||
{% set current_areas = (supplier.service_area or '').split(',') %}
|
||||
{% for c in countries %}
|
||||
@@ -165,7 +165,7 @@
|
||||
|
||||
<div class="lst-row">
|
||||
<div>
|
||||
<label class="lst-label">Contact Role / Title</label>
|
||||
<label class="lst-label">{{ t.sd_lst_contact_role }}</label>
|
||||
<input type="text" name="contact_role" value="{{ supplier.contact_role or '' }}" class="lst-input"
|
||||
placeholder="e.g. International Sales, Managing Director">
|
||||
</div>
|
||||
@@ -173,7 +173,7 @@
|
||||
</div>
|
||||
|
||||
<div class="lst-full" style="margin-top:0.25rem">
|
||||
<label class="lst-label">Services Offered</label>
|
||||
<label class="lst-label">{{ t.sd_lst_services_offered }}</label>
|
||||
<div class="lst-pills">
|
||||
{% set current_services = (supplier.services_offered or '').split(',') %}
|
||||
{% set service_options = [
|
||||
@@ -199,7 +199,7 @@
|
||||
</div>
|
||||
|
||||
<div class="lst-full" style="margin-top:0.75rem">
|
||||
<label class="lst-label">Social Links</label>
|
||||
<label class="lst-label">{{ t.sd_lst_social_links }}</label>
|
||||
<div style="display:flex;flex-direction:column;gap:0.5rem">
|
||||
<input type="url" name="linkedin_url" value="{{ supplier.linkedin_url or '' }}" class="lst-input"
|
||||
placeholder="https://linkedin.com/company/...">
|
||||
@@ -211,7 +211,7 @@
|
||||
</div>
|
||||
|
||||
<div style="margin-top:1.5rem">
|
||||
<button type="submit" class="btn">Save Changes</button>
|
||||
<button type="submit" class="btn">{{ t.sd_lst_save }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<div class="lst-badges">
|
||||
<span class="lst-badge lst-badge--tier">{{ supplier.tier | upper }}</span>
|
||||
{% if 'verified' in active_boosts or supplier.is_verified %}
|
||||
<span class="lst-badge lst-badge--verified">Verified ✓</span>
|
||||
<span class="lst-badge lst-badge--verified">{{ t.sd_lst_verified }} ✓</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if supplier.tagline %}
|
||||
|
||||
@@ -33,53 +33,53 @@
|
||||
{% if supplier.tier in ('growth', 'pro') and new_leads_count > 0 %}
|
||||
<div class="ov-alert">
|
||||
<span class="ov-alert__count">{{ new_leads_count }}</span>
|
||||
<span>new lead{{ 's' if new_leads_count != 1 }} match your profile.
|
||||
<span>{{ t.sd_ov_new_leads_text }}
|
||||
<a href="{{ url_for('suppliers.dashboard', tab='leads') }}"
|
||||
hx-get="{{ url_for('suppliers.dashboard_leads') }}"
|
||||
hx-target="#dashboard-content"
|
||||
hx-push-url="{{ url_for('suppliers.dashboard', tab='leads') }}"
|
||||
style="font-weight:600">View Lead Feed →</a>
|
||||
style="font-weight:600">{{ t.sd_ov_view_lead_feed }} →</a>
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="ov-stats">
|
||||
<div class="ov-stat">
|
||||
<div class="ov-stat__label">Profile Views</div>
|
||||
<div class="ov-stat__label">{{ t.sd_ov_profile_views }}</div>
|
||||
<div class="ov-stat__value">—</div>
|
||||
<div style="font-size:0.6875rem;color:#94A3B8;margin-top:2px">via Umami</div>
|
||||
<div style="font-size:0.6875rem;color:#94A3B8;margin-top:2px">{{ t.sd_ov_via_umami }}</div>
|
||||
</div>
|
||||
{% if supplier.tier == 'basic' %}
|
||||
<div class="ov-stat">
|
||||
<div class="ov-stat__label">Enquiries Received</div>
|
||||
<div class="ov-stat__label">{{ t.sd_ov_enquiries }}</div>
|
||||
<div class="ov-stat__value">{{ enquiry_count }}</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="ov-stat">
|
||||
<div class="ov-stat__label">Leads Unlocked</div>
|
||||
<div class="ov-stat__label">{{ t.sd_ov_leads_unlocked }}</div>
|
||||
<div class="ov-stat__value">{{ leads_unlocked }}</div>
|
||||
</div>
|
||||
<div class="ov-stat">
|
||||
<div class="ov-stat__label">Credits Balance</div>
|
||||
<div class="ov-stat__label">{{ t.sd_ov_credits_balance }}</div>
|
||||
<div class="ov-stat__value ov-stat__value--blue">{{ supplier.credit_balance }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="ov-stat">
|
||||
<div class="ov-stat__label">Directory Rank</div>
|
||||
<div class="ov-stat__label">{{ t.sd_ov_directory_rank }}</div>
|
||||
<div class="ov-stat__value">—</div>
|
||||
<div style="font-size:0.6875rem;color:#94A3B8;margin-top:2px">via Umami</div>
|
||||
<div style="font-size:0.6875rem;color:#94A3B8;margin-top:2px">{{ t.sd_ov_via_umami }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if supplier.tier == 'basic' %}
|
||||
<div style="background:#EFF6FF;border:1px solid #BFDBFE;border-radius:10px;padding:12px 16px;margin-bottom:1.5rem;font-size:0.8125rem;color:#1D4ED8">
|
||||
<strong>Basic plan</strong> — You have a verified listing with an enquiry form. Upgrade to Growth to access qualified project leads.
|
||||
<a href="{{ url_for('suppliers.signup') }}" style="display:block;font-weight:600;margin-top:4px;color:#1D4ED8">Upgrade to Growth →</a>
|
||||
<strong>{{ t.sd_ov_basic_plan_label }}</strong> — {{ t.sd_ov_basic_plan_desc }}
|
||||
<a href="{{ url_for('suppliers.signup') }}" style="display:block;font-weight:600;margin-top:4px;color:#1D4ED8">{{ t.sd_ov_upgrade_growth }} →</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="ov-activity">
|
||||
<h3>Recent Activity</h3>
|
||||
<h3>{{ t.sd_ov_recent_activity }}</h3>
|
||||
{% if recent_activity %}
|
||||
{% for item in recent_activity %}
|
||||
<div class="ov-activity__item">
|
||||
@@ -88,11 +88,11 @@
|
||||
<span class="ov-activity__time">{{ item.created_at[:16] }}</span>
|
||||
</div>
|
||||
<span class="ov-activity__delta {% if item.delta < 0 %}ov-activity__delta--neg{% else %}ov-activity__delta--pos{% endif %}">
|
||||
{{ '%+d' % item.delta }} credits
|
||||
{{ '%+d' % item.delta }} {{ t.sd_ov_credits }}
|
||||
</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p style="color:#94A3B8;font-size:0.8125rem;text-align:center;padding:1rem 0">No activity yet. Unlock your first lead to get started.</p>
|
||||
<p style="color:#94A3B8;font-size:0.8125rem;text-align:center;padding:1rem 0">{{ t.sd_ov_no_activity }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
<div class="lf-card">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.5rem">
|
||||
<span class="lf-card__heat lf-card__heat--{{ lead.heat_score or 'cool' }}">{{ (lead.heat_score or 'cool') | upper }}</span>
|
||||
<span class="lf-card__unlocks">{{ lead.unlock_count or 0 }} supplier{{ 's' if (lead.unlock_count or 0) != 1 }} unlocked</span>
|
||||
<span class="lf-card__unlocks">{{ lead.unlock_count or 0 }} {{ t.sd_card_unlocks }}</span>
|
||||
</div>
|
||||
|
||||
<dl class="lf-card__meta">
|
||||
<dt>Facility</dt>
|
||||
<dt>{{ t.sd_card_facility }}</dt>
|
||||
<dd>{{ lead.facility_type or '-' }}</dd>
|
||||
<dt>Courts</dt>
|
||||
<dt>{{ t.sd_card_courts }}</dt>
|
||||
<dd>{{ lead.court_count or '-' }}</dd>
|
||||
<dt>Country</dt>
|
||||
<dt>{{ t.sd_card_country }}</dt>
|
||||
<dd>{{ lead.country or '-' }}</dd>
|
||||
<dt>Timeline</dt>
|
||||
<dt>{{ t.sd_card_timeline }}</dt>
|
||||
<dd>{{ lead.timeline or '-' }}</dd>
|
||||
<dt>Budget</dt>
|
||||
<dt>{{ t.sd_card_budget }}</dt>
|
||||
<dd>{% if lead.budget_estimate %}~€{{ ((lead.budget_estimate | int / 1000) | round | int) }}K{% else %}-{% endif %}</dd>
|
||||
<dt>Context</dt>
|
||||
<dt>{{ t.sd_card_context }}</dt>
|
||||
<dd>{{ lead.build_context or '-' }}</dd>
|
||||
</dl>
|
||||
|
||||
{% if lead.services_needed %}
|
||||
<p style="font-size:0.6875rem;color:#64748B;margin:0 0 0.5rem">Services: {{ lead.services_needed }}</p>
|
||||
<p style="font-size:0.6875rem;color:#64748B;margin:0 0 0.5rem">{{ t.sd_card_services }} {{ lead.services_needed }}</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="lf-card__foot">
|
||||
<span class="lf-card__cost"><strong>{{ lead.credit_cost or '?' }}</strong> credits</span>
|
||||
<span class="lf-card__cost"><strong>{{ lead.credit_cost or '?' }}</strong> {{ t.sd_card_credits }}</span>
|
||||
<form hx-post="{{ url_for('suppliers.unlock_lead', lead_id=lead.id) }}"
|
||||
hx-target="#lead-card-{{ lead.id }}" hx-swap="innerHTML">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="lf-unlock-btn">Unlock Lead</button>
|
||||
<button type="submit" class="lf-unlock-btn">{{ t.sd_card_unlock_btn }}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="lf-card" style="border-color:#FCA5A5">
|
||||
<div style="text-align:center;padding:0.75rem 0">
|
||||
<p style="font-size:0.8125rem;font-weight:600;color:#DC2626;margin:0 0 4px">Not enough credits</p>
|
||||
<p style="font-size:0.75rem;color:#64748B;margin:0 0 12px">You have <strong>{{ balance }}</strong> credits, this lead costs <strong>{{ required }}</strong>.</p>
|
||||
<a href="{{ url_for('suppliers.dashboard', tab='boosts') }}" class="lf-unlock-btn" style="display:inline-block;text-decoration:none">Buy Credits</a>
|
||||
<p style="font-size:0.8125rem;font-weight:600;color:#DC2626;margin:0 0 4px">{{ t.sd_error_not_enough }}</p>
|
||||
<p style="font-size:0.75rem;color:#64748B;margin:0 0 12px">{{ t.sd_error_credit_msg | tformat(balance=balance, required=required) }}</p>
|
||||
<a href="{{ url_for('suppliers.dashboard', tab='boosts') }}" class="lf-unlock-btn" style="display:inline-block;text-decoration:none">{{ t.sd_error_buy }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,84 +1,84 @@
|
||||
{# Human-readable labels for enum values #}
|
||||
{% set timeline_labels = {'asap': 'ASAP', '3_6_months': '3-6 months', '6_12_months': '6-12 months', '12_plus': '12+ months', 'exploring': 'Exploring'} %}
|
||||
{% set phase_labels = {'permit_granted': 'Permit granted', 'lease_signed': 'Lease signed', 'permit_pending': 'Permit pending', 'converting_existing': 'Converting existing', 'permit_not_filed': 'Permit not filed', 'location_found': 'Location found', 'searching': 'Searching'} %}
|
||||
{% set financing_labels = {'self_funded': 'Self-funded', 'loan_approved': 'Loan approved', 'seeking': 'Seeking financing', 'not_started': 'Not started'} %}
|
||||
{% set decision_labels = {'solo': 'Solo decision-maker', 'partners': 'With partners', 'board': 'Board/committee', 'investor': 'Investor-led'} %}
|
||||
{% set contact_labels = {'received_quotes': 'Has received quotes', 'contacted': 'Has contacted suppliers', 'none': 'No prior contact'} %}
|
||||
{% set stakeholder_labels = {'owner': 'Owner/Operator', 'investor': 'Investor', 'developer': 'Property Developer', 'club': 'Club/Association', 'other': 'Other'} %}
|
||||
{# Human-readable labels for enum values — resolved from translation keys #}
|
||||
{% set timeline_labels = {'asap': t.sd_timeline_asap, '3_6_months': t.sd_timeline_3_6mo, '6_12_months': t.sd_timeline_6_12mo, '12_plus': t.sd_timeline_12plus, 'exploring': t.sd_timeline_exploring} %}
|
||||
{% set phase_labels = {'permit_granted': t.sd_phase_permit_granted, 'lease_signed': t.sd_phase_lease_signed, 'permit_pending': t.sd_phase_permit_pending, 'converting_existing': t.sd_phase_converting, 'permit_not_filed': t.sd_phase_permit_not_filed, 'location_found': t.sd_phase_location_found, 'searching': t.sd_phase_searching} %}
|
||||
{% set financing_labels = {'self_funded': t.sd_financing_self_funded, 'loan_approved': t.sd_financing_loan_approved, 'seeking': t.sd_financing_seeking, 'not_started': t.sd_financing_not_started} %}
|
||||
{% set decision_labels = {'solo': t.sd_decision_solo, 'partners': t.sd_decision_partners, 'board': t.sd_decision_board, 'investor': t.sd_decision_investor} %}
|
||||
{% set contact_labels = {'received_quotes': t.sd_contact_received_quotes, 'contacted': t.sd_contact_contacted, 'none': t.sd_contact_none} %}
|
||||
{% set stakeholder_labels = {'owner': t.sd_stakeholder_owner, 'investor': t.sd_stakeholder_investor, 'developer': t.sd_stakeholder_developer, 'club': t.sd_stakeholder_club, 'other': t.sd_stakeholder_other} %}
|
||||
|
||||
<div class="lf-card lf-card--unlocked">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.75rem">
|
||||
<span class="lf-card__heat lf-card__heat--{{ lead.heat_score or 'cool' }}">{{ (lead.heat_score or 'cool') | upper }}</span>
|
||||
<span style="font-size:0.6875rem;color:#16A34A;font-weight:600">✓ Unlocked</span>
|
||||
<span style="font-size:0.6875rem;color:#16A34A;font-weight:600">✓ {{ t.sd_unlocked_badge }}</span>
|
||||
</div>
|
||||
|
||||
{# --- Project --- #}
|
||||
<div class="lf-section">
|
||||
<div class="lf-section__title">Project</div>
|
||||
<div class="lf-section__title">{{ t.sd_unlocked_section_project }}</div>
|
||||
<dl class="lf-card__meta">
|
||||
<dt>Facility</dt>
|
||||
<dt>{{ t.sd_unlocked_label_facility }}</dt>
|
||||
<dd>{{ lead.facility_type or '-' }}{% if lead.build_context %} ({{ lead.build_context }}){% endif %}</dd>
|
||||
<dt>Courts</dt>
|
||||
<dt>{{ t.sd_unlocked_label_courts }}</dt>
|
||||
<dd>{{ lead.court_count or '-' }}</dd>
|
||||
<dt>Glass</dt>
|
||||
<dt>{{ t.sd_unlocked_label_glass }}</dt>
|
||||
<dd>{{ lead.glass_type or '-' }}</dd>
|
||||
<dt>Lighting</dt>
|
||||
<dt>{{ t.sd_unlocked_label_lighting }}</dt>
|
||||
<dd>{{ lead.lighting_type or '-' }}</dd>
|
||||
<dt>Budget</dt>
|
||||
<dt>{{ t.sd_unlocked_label_budget }}</dt>
|
||||
<dd>{% if lead.budget_estimate %}€{{ lead.budget_estimate }}{% else %}-{% endif %}</dd>
|
||||
<dt>Services</dt>
|
||||
<dt>{{ t.sd_unlocked_label_services }}</dt>
|
||||
<dd>{{ lead.services_needed or '-' }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
{# --- Location & Timeline --- #}
|
||||
<div class="lf-section">
|
||||
<div class="lf-section__title">Location & Timeline</div>
|
||||
<div class="lf-section__title">{{ t.sd_unlocked_section_location }}</div>
|
||||
<dl class="lf-card__meta">
|
||||
<dt>Location</dt>
|
||||
<dt>{{ t.sd_unlocked_label_location }}</dt>
|
||||
<dd>{{ lead.location or '-' }}, {{ lead.country or '-' }}</dd>
|
||||
<dt>Timeline</dt>
|
||||
<dt>{{ t.sd_unlocked_label_timeline }}</dt>
|
||||
<dd>{{ timeline_labels.get(lead.timeline, lead.timeline) or '-' }}</dd>
|
||||
<dt>Phase</dt>
|
||||
<dt>{{ t.sd_unlocked_label_phase }}</dt>
|
||||
<dd>{{ phase_labels.get(lead.location_status, lead.location_status) or '-' }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
{# --- Readiness --- #}
|
||||
<div class="lf-section">
|
||||
<div class="lf-section__title">Readiness</div>
|
||||
<div class="lf-section__title">{{ t.sd_unlocked_section_readiness }}</div>
|
||||
<dl class="lf-card__meta">
|
||||
<dt>Financing</dt>
|
||||
<dt>{{ t.sd_unlocked_label_financing }}</dt>
|
||||
<dd>{{ financing_labels.get(lead.financing_status, lead.financing_status) or '-' }}</dd>
|
||||
<dt>Wants financing help</dt>
|
||||
<dd>{{ 'Yes' if lead.wants_financing_help else 'No' }}</dd>
|
||||
<dt>Decision process</dt>
|
||||
<dt>{{ t.sd_unlocked_label_wants_financing }}</dt>
|
||||
<dd>{{ t.sd_unlocked_yes if lead.wants_financing_help else t.sd_unlocked_no }}</dd>
|
||||
<dt>{{ t.sd_unlocked_label_decision }}</dt>
|
||||
<dd>{{ decision_labels.get(lead.decision_process, lead.decision_process) or '-' }}</dd>
|
||||
<dt>Prior supplier contact</dt>
|
||||
<dt>{{ t.sd_unlocked_label_prior_contact }}</dt>
|
||||
<dd>{{ contact_labels.get(lead.previous_supplier_contact, lead.previous_supplier_contact) or '-' }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
{% if lead.additional_info %}
|
||||
<div class="lf-section">
|
||||
<div class="lf-section__title">Notes</div>
|
||||
<div class="lf-section__title">{{ t.sd_unlocked_section_notes }}</div>
|
||||
<p style="font-size:0.75rem;color:#475569;background:#F8FAFC;padding:8px;border-radius:6px;margin:0">{{ lead.additional_info }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# --- Contact --- #}
|
||||
<div class="lf-contact">
|
||||
<div class="lf-section__title" style="margin-bottom:6px">Contact</div>
|
||||
<div class="lf-section__title" style="margin-bottom:6px">{{ t.sd_unlocked_section_contact }}</div>
|
||||
<dl style="display:grid;grid-template-columns:80px 1fr;gap:2px 8px">
|
||||
<dt>Name</dt>
|
||||
<dt>{{ t.sd_unlocked_label_name }}</dt>
|
||||
<dd>{{ lead.contact_name or '-' }}</dd>
|
||||
<dt>Email</dt>
|
||||
<dt>{{ t.sd_unlocked_label_email }}</dt>
|
||||
<dd><a href="mailto:{{ lead.contact_email }}" style="color:#1D4ED8">{{ lead.contact_email or '-' }}</a></dd>
|
||||
<dt>Phone</dt>
|
||||
<dt>{{ t.sd_unlocked_label_phone }}</dt>
|
||||
<dd>{% if lead.contact_phone %}<a href="tel:{{ lead.contact_phone }}" style="color:#1D4ED8">{{ lead.contact_phone }}</a>{% else %}-{% endif %}</dd>
|
||||
<dt>Company</dt>
|
||||
<dt>{{ t.sd_unlocked_label_company }}</dt>
|
||||
<dd>{{ lead.contact_company or '-' }}</dd>
|
||||
<dt>Role</dt>
|
||||
<dt>{{ t.sd_unlocked_label_role }}</dt>
|
||||
<dd>{{ stakeholder_labels.get(lead.stakeholder_type, lead.stakeholder_type) or '-' }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
@@ -88,26 +88,26 @@
|
||||
{% if sid %}
|
||||
<a href="{{ url_for('planner.index') }}?scenario={{ sid }}" target="_blank"
|
||||
style="display:block;text-align:center;margin-top:0.75rem;font-size:0.75rem;font-weight:600;color:#1D4ED8;text-decoration:none">
|
||||
View their plan →
|
||||
{{ t.sd_unlocked_view_plan }} →
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if credit_cost is defined %}
|
||||
<p style="font-size:0.6875rem;color:#94A3B8;margin-top:0.5rem;text-align:center">{{ credit_cost }} credits used · {{ supplier.credit_balance }} remaining</p>
|
||||
<p style="font-size:0.6875rem;color:#94A3B8;margin-top:0.5rem;text-align:center">{{ credit_cost }} {{ t.sd_unlocked_credits_used }} · {{ supplier.credit_balance }} {{ t.sd_unlocked_remaining }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if credit_cost is defined %}
|
||||
{# OOB: update sidebar credits #}
|
||||
<div id="sidebar-credits" hx-swap-oob="innerHTML">
|
||||
{{ supplier.credit_balance }} credits
|
||||
{{ supplier.credit_balance }} {{ t.sd_unlocked_credits }}
|
||||
</div>
|
||||
{# OOB: update header credits #}
|
||||
<div id="dl-credit-balance" hx-swap-oob="innerHTML">
|
||||
{{ supplier.credit_balance }} credits
|
||||
{{ supplier.credit_balance }} {{ t.sd_unlocked_credits }}
|
||||
<a href="{{ url_for('suppliers.dashboard', tab='boosts') }}"
|
||||
hx-get="{{ url_for('suppliers.dashboard_boosts') }}"
|
||||
hx-target="#dashboard-content"
|
||||
hx-push-url="{{ url_for('suppliers.dashboard', tab='boosts') }}">Buy More</a>
|
||||
hx-push-url="{{ url_for('suppliers.dashboard', tab='boosts') }}">{{ t.sd_unlocked_buy_more }}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div data-step="1">
|
||||
<h2 class="s-step-title">{% if lang == 'de' %}Plan auswählen{% else %}Choose Your Plan{% endif %}</h2>
|
||||
<p class="s-step-sub">{% if lang == 'de' %}Wähle den Plan, der zu deinen Wachstumszielen passt.{% else %}Select the plan that fits your growth goals.{% endif %}</p>
|
||||
<h2 class="s-step-title">{{ t.sup_step1_title }}</h2>
|
||||
<p class="s-step-sub">{{ t.sup_step1_sub }}</p>
|
||||
|
||||
<!-- Billing period toggle (CSS sibling selector trick).
|
||||
Radios must be direct siblings of .s-billing-toggle AND <form> so that
|
||||
@@ -11,9 +11,9 @@
|
||||
{% if data.get('billing_period', 'yearly') != 'monthly' %}checked{% endif %}>
|
||||
<div class="s-billing-toggle">
|
||||
<div class="s-billing-toggle__pill">
|
||||
<label for="bp-monthly" class="s-billing-toggle__opt">{% if lang == 'de' %}Monatlich{% else %}Monthly{% endif %}</label>
|
||||
<label for="bp-monthly" class="s-billing-toggle__opt">{{ t.sup_step1_monthly }}</label>
|
||||
<label for="bp-yearly" class="s-billing-toggle__opt">
|
||||
{% if lang == 'de' %}Jährlich{% else %}Yearly{% endif %} <span class="s-save-badge">{% if lang == 'de' %}Bis zu 26 % sparen{% else %}Save up to 26%{% endif %}</span>
|
||||
{{ t.sup_step1_yearly }} <span class="s-save-badge">{{ t.sup_step1_save_badge }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -61,19 +61,19 @@
|
||||
<label class="s-plan-card {% if data.get('plan', 'supplier_growth') == key %}s-plan-card--selected{% endif %}"
|
||||
onclick="this.parentNode.querySelectorAll('.s-plan-card').forEach(c=>c.classList.remove('s-plan-card--selected')); this.classList.add('s-plan-card--selected')">
|
||||
<input type="radio" name="plan" value="{{ key }}" {% if data.get('plan', 'supplier_growth') == key %}checked{% endif %}>
|
||||
{% if key == 'supplier_growth' %}<div class="s-plan-card__popular">{% if lang == 'de' %}Beliebtester{% else %}Most Popular{% endif %}</div>{% endif %}
|
||||
{% if key == 'supplier_growth' %}<div class="s-plan-card__popular">{{ t.sup_step1_popular }}</div>{% endif %}
|
||||
<h3>{{ plan.name }}</h3>
|
||||
<div class="price-yearly">
|
||||
<div class="price">€{{ plan.yearly_monthly_equivalent }} <span>/mo</span></div>
|
||||
<div style="font-size:0.6875rem;color:#94A3B8;margin-top:2px">{% if lang == 'de' %}jährl. €{{ plan.yearly_price }}{% else %}billed at €{{ plan.yearly_price }}/yr{% endif %}</div>
|
||||
<div style="font-size:0.6875rem;color:#94A3B8;margin-top:2px">{{ t.sup_step1_billed_yearly | tformat(price=plan.yearly_price) }}</div>
|
||||
</div>
|
||||
<div class="price-monthly">
|
||||
<div class="price">€{{ plan.monthly_price }} <span>/mo</span></div>
|
||||
<div style="font-size:0.6875rem;color:#94A3B8;margin-top:2px">{% if lang == 'de' %}monatliche Abrechnung{% else %}billed monthly{% endif %}</div>
|
||||
<div style="font-size:0.6875rem;color:#94A3B8;margin-top:2px">{{ t.sup_step1_billed_monthly }}</div>
|
||||
</div>
|
||||
<ul>
|
||||
{% for f in (plan.features_de if lang == 'de' else plan.features) %}
|
||||
<li>{{ f }}</li>
|
||||
{% for key in plan.feature_keys %}
|
||||
<li>{{ t[key] }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</label>
|
||||
@@ -82,7 +82,7 @@
|
||||
|
||||
<div class="s-nav">
|
||||
<span></span>
|
||||
<button type="submit" class="s-btn-next">{% if lang == 'de' %}Weiter: Add-ons{% else %}Next: Add-Ons{% endif %}</button>
|
||||
<button type="submit" class="s-btn-next">{{ t.sup_step1_next }}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div data-step="2">
|
||||
<h2 class="s-step-title">{% if lang == 'de' %}Boost-Add-ons{% else %}Boost Add-Ons{% endif %}</h2>
|
||||
<p class="s-step-sub">{% if lang == 'de' %}Erhöhe deine Sichtbarkeit mit optionalen Boosts. {% if included_boosts %}Einige sind in deinem Plan enthalten.{% endif %}{% else %}Increase your visibility with optional boosts. {% if included_boosts %}Some are included in your plan.{% endif %}{% endif %}</p>
|
||||
<h2 class="s-step-title">{{ t.sup_step2_title }}</h2>
|
||||
<p class="s-step-sub">{{ t.sup_step2_sub_pre }} {% if included_boosts %}{{ t.sup_step2_sub_included }}{% endif %}</p>
|
||||
|
||||
<form hx-post="{{ url_for('suppliers.signup_step', step=2) }}"
|
||||
hx-target="#signup-step" hx-swap="innerHTML">
|
||||
@@ -21,7 +21,7 @@
|
||||
{% endif %}
|
||||
<strong>{{ b.name }}</strong>
|
||||
{% if is_included %}
|
||||
<span class="boost-included">{% if lang == 'de' %}Im Plan enthalten{% else %}Included in plan{% endif %}</span>
|
||||
<span class="boost-included">{{ t.sup_step2_included }}</span>
|
||||
{% else %}
|
||||
<span class="boost-price">€{{ b.price }}/mo</span>
|
||||
{% endif %}
|
||||
@@ -33,8 +33,8 @@
|
||||
<div class="s-nav">
|
||||
<button type="button" class="s-btn-back"
|
||||
hx-get="{{ url_for('suppliers.signup_step', step=1) }}?_accumulated={{ data | tojson | urlencode }}"
|
||||
hx-target="#signup-step" hx-swap="innerHTML">{% if lang == 'de' %}Zurück{% else %}Back{% endif %}</button>
|
||||
<button type="submit" class="s-btn-next">{% if lang == 'de' %}Weiter: Credit-Pakete{% else %}Next: Credit Packs{% endif %}</button>
|
||||
hx-target="#signup-step" hx-swap="innerHTML">{{ t.sup_btn_back }}</button>
|
||||
<button type="submit" class="s-btn-next">{{ t.sup_step2_next }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div data-step="3">
|
||||
<h2 class="s-step-title">{% if lang == 'de' %}Credit-Pakete{% else %}Credit Packs{% endif %}</h2>
|
||||
<p class="s-step-sub">{% if lang == 'de' %}Optional deine Lead-Credits aufstocken. Dein Plan enthält monatliche Credits — Pakete geben dir zusätzliche.{% else %}Optionally top up your lead credits. Your plan includes monthly credits — packs give you extra.{% endif %}</p>
|
||||
<h2 class="s-step-title">{{ t.sup_step3_title }}</h2>
|
||||
<p class="s-step-sub">{{ t.sup_step3_sub }}</p>
|
||||
|
||||
<form hx-post="{{ url_for('suppliers.signup_step', step=3) }}"
|
||||
hx-target="#signup-step" hx-swap="innerHTML">
|
||||
@@ -12,8 +12,8 @@
|
||||
onclick="this.parentNode.querySelectorAll('.s-credit-card').forEach(c=>c.classList.remove('s-credit-card--selected')); this.classList.add('s-credit-card--selected')">
|
||||
<input type="radio" name="credit_pack" value="" {% if not data.get('credit_pack') %}checked{% endif %}>
|
||||
<div class="amount">0</div>
|
||||
<div class="price">{% if lang == 'de' %}Kostenlos{% else %}Free{% endif %}</div>
|
||||
<div class="per">{% if lang == 'de' %}Nur Plan-Credits{% else %}Plan credits only{% endif %}</div>
|
||||
<div class="price">{{ t.sup_step3_free }}</div>
|
||||
<div class="per">{{ t.sup_step3_free_desc }}</div>
|
||||
</label>
|
||||
{% for cp in credit_packs %}
|
||||
<label class="s-credit-card {% if data.get('credit_pack') == cp.key %}s-credit-card--selected{% endif %}"
|
||||
@@ -30,8 +30,8 @@
|
||||
<button type="button" class="s-btn-back"
|
||||
hx-post="{{ url_for('suppliers.signup_step', step=1) }}"
|
||||
hx-target="#signup-step" hx-swap="innerHTML"
|
||||
hx-include="[name='_accumulated']">{% if lang == 'de' %}Zurück{% else %}Back{% endif %}</button>
|
||||
<button type="submit" class="s-btn-next">{% if lang == 'de' %}Weiter: Deine Daten{% else %}Next: Your Details{% endif %}</button>
|
||||
hx-include="[name='_accumulated']">{{ t.sup_btn_back }}</button>
|
||||
<button type="submit" class="s-btn-next">{{ t.sup_step3_next }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div data-step="4">
|
||||
<h2 class="s-step-title">{% if lang == 'de' %}Kontodaten{% else %}Account Details{% endif %}</h2>
|
||||
<p class="s-step-sub">{% if lang == 'de' %}Erzähl uns von deinem Unternehmen und wie wir dich erreichen können.{% else %}Tell us about your company and how to reach you.{% endif %}</p>
|
||||
<h2 class="s-step-title">{{ t.sup_step4_title }}</h2>
|
||||
<p class="s-step-sub">{{ t.sup_step4_sub }}</p>
|
||||
|
||||
<form method="post" action="{{ url_for('suppliers.signup_checkout') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
@@ -8,39 +8,39 @@
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0 1rem">
|
||||
<div class="s-field">
|
||||
<label class="s-label">{% if lang == 'de' %}Ansprechpartner{% else %}Contact Name{% endif %} <span style="color:#EF4444">*</span></label>
|
||||
<label class="s-label">{{ t.sup_step4_contact_name }} <span style="color:#EF4444">*</span></label>
|
||||
<input type="text" name="contact_name" class="s-input" value="{{ data.get('contact_name', '') }}" required>
|
||||
</div>
|
||||
<div class="s-field">
|
||||
<label class="s-label">{% if lang == 'de' %}E-Mail{% else %}Email{% endif %} <span style="color:#EF4444">*</span></label>
|
||||
<label class="s-label">{{ t.sup_step4_email }} <span style="color:#EF4444">*</span></label>
|
||||
<input type="email" name="contact_email" class="s-input" value="{{ data.get('contact_email', '') }}" required>
|
||||
</div>
|
||||
<div class="s-field">
|
||||
<label class="s-label">{% if lang == 'de' %}Telefon{% else %}Phone{% endif %}</label>
|
||||
<label class="s-label">{{ t.sup_step4_phone }}</label>
|
||||
<input type="tel" name="contact_phone" class="s-input" value="{{ data.get('contact_phone', '') }}">
|
||||
</div>
|
||||
<div class="s-field">
|
||||
<label class="s-label">{% if lang == 'de' %}Kurzbeschreibung{% else %}Short Description{% endif %}</label>
|
||||
<label class="s-label">{{ t.sup_step4_short_desc }}</label>
|
||||
<input type="text" name="short_description" class="s-input" maxlength="160"
|
||||
value="{{ data.get('short_description', '') }}" placeholder="{% if lang == 'de' %}max. 160 Zeichen{% else %}160 chars max{% endif %}">
|
||||
value="{{ data.get('short_description', '') }}" placeholder="{{ t.sup_step4_short_desc_ph }}">
|
||||
</div>
|
||||
<div class="s-field">
|
||||
<label class="s-label">{% if lang == 'de' %}Leistungskategorien{% else %}Service Categories{% endif %}</label>
|
||||
<label class="s-label">{{ t.sup_step4_service_cats }}</label>
|
||||
<input type="text" name="service_categories" class="s-input"
|
||||
value="{{ data.get('service_categories', '') }}" placeholder="{% if lang == 'de' %}z.B. schlüsselfertig, Beläge, Beleuchtung{% else %}e.g. turnkey, surfaces, lighting{% endif %}">
|
||||
value="{{ data.get('service_categories', '') }}" placeholder="{{ t.sup_step4_service_cats_ph }}">
|
||||
</div>
|
||||
<div class="s-field">
|
||||
<label class="s-label">{% if lang == 'de' %}Servicegebiet (Länder){% else %}Service Area (countries){% endif %}</label>
|
||||
<label class="s-label">{{ t.sup_step4_service_area }}</label>
|
||||
<input type="text" name="service_area" class="s-input"
|
||||
value="{{ data.get('service_area', '') }}" placeholder="e.g. DE, ES, FR">
|
||||
</div>
|
||||
<div class="s-field">
|
||||
<label class="s-label">{% if lang == 'de' %}Jahre im Geschäft{% else %}Years in Business{% endif %}</label>
|
||||
<label class="s-label">{{ t.sup_step4_years }}</label>
|
||||
<input type="number" name="years_in_business" class="s-input" min="0"
|
||||
value="{{ data.get('years_in_business', '') }}">
|
||||
</div>
|
||||
<div class="s-field">
|
||||
<label class="s-label">{% if lang == 'de' %}Anzahl Projekte{% else %}Project Count{% endif %}</label>
|
||||
<label class="s-label">{{ t.sup_step4_projects }}</label>
|
||||
<input type="number" name="project_count" class="s-input" min="0"
|
||||
value="{{ data.get('project_count', '') }}">
|
||||
</div>
|
||||
@@ -48,13 +48,13 @@
|
||||
|
||||
{% if data.get('supplier_name') %}
|
||||
<p style="font-size:12px;color:#64748B;margin-top:0.5rem">
|
||||
{% if lang == 'de' %}Eintrag beanspruchen:{% else %}Claiming listing:{% endif %} <strong>{{ data.get('supplier_name') }}</strong>
|
||||
{{ t.sup_step4_claiming }} <strong>{{ data.get('supplier_name') }}</strong>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<!-- Order Summary -->
|
||||
<div class="s-summary">
|
||||
<h3>{% if lang == 'de' %}Bestellübersicht{% else %}Order Summary{% endif %}</h3>
|
||||
<h3>{{ t.sup_step4_order_h3 }}</h3>
|
||||
<div class="s-summary-row">
|
||||
<span>{{ order.plan_name }} Plan</span>
|
||||
<span>
|
||||
@@ -71,17 +71,17 @@
|
||||
</div>
|
||||
{% if order.boost_monthly > 0 %}
|
||||
<div class="s-summary-row">
|
||||
<span>{% if lang == 'de' %}Boost-Add-ons{% else %}Boost add-ons{% endif %}</span>
|
||||
<span>{{ t.sup_step4_boost_row }}</span>
|
||||
<span>+€{{ order.boost_monthly }}/mo</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="s-summary-row s-summary-total">
|
||||
<span>{% if order.billing_period == 'yearly' %}{% if lang == 'de' %}Jahresgesamt{% else %}Yearly total{% endif %}{% else %}{% if lang == 'de' %}Monatsgesamt{% else %}Monthly total{% endif %}{% endif %}</span>
|
||||
<span>€{{ order.monthly_total }}{% if order.billing_period == 'yearly' %}{% if lang == 'de' %}/Mon. äquiv.{% else %}/mo equiv.{% endif %}{% else %}/mo{% endif %}</span>
|
||||
<span>{% if order.billing_period == 'yearly' %}{{ t.sup_step4_total_yearly }}{% else %}{{ t.sup_step4_total_monthly }}{% endif %}</span>
|
||||
<span>€{{ order.monthly_total }}{% if order.billing_period == 'yearly' %}{{ t.sup_step4_equiv }}{% else %}/mo{% endif %}</span>
|
||||
</div>
|
||||
{% if order.one_time_total > 0 %}
|
||||
<div class="s-summary-row" style="margin-top:8px">
|
||||
<span>{% if lang == 'de' %}Credit-Paket (einmalig){% else %}Credit pack (one-time){% endif %}</span>
|
||||
<span>{{ t.sup_step4_credit_row }}</span>
|
||||
<span>€{{ order.one_time_total }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -91,8 +91,8 @@
|
||||
<button type="button" class="s-btn-back"
|
||||
hx-post="{{ url_for('suppliers.signup_step', step=2) }}"
|
||||
hx-target="#signup-step" hx-swap="innerHTML"
|
||||
hx-include="[name='_accumulated']">{% if lang == 'de' %}Zurück{% else %}Back{% endif %}</button>
|
||||
<button type="submit" class="s-btn-next" id="checkout-btn">{% if lang == 'de' %}Zur Kasse{% else %}Proceed to Checkout{% endif %}</button>
|
||||
hx-include="[name='_accumulated']">{{ t.sup_btn_back }}</button>
|
||||
<button type="submit" class="s-btn-next" id="checkout-btn">{{ t.sup_step4_checkout }}</button>
|
||||
</div>
|
||||
|
||||
<div id="checkout-error" hidden
|
||||
@@ -107,7 +107,7 @@
|
||||
var btn = document.getElementById('checkout-btn');
|
||||
var errBox = document.getElementById('checkout-error');
|
||||
btn.disabled = true;
|
||||
btn.textContent = '{% if lang == 'de' %}Wird geladen\u2026{% else %}Loading\u2026{% endif %}';
|
||||
btn.textContent = {{ t.sup_step4_loading | tojson }};
|
||||
errBox.hidden = true;
|
||||
|
||||
fetch(form.action, {
|
||||
@@ -118,10 +118,10 @@
|
||||
.then(function(res) { return res.json().then(function(d) { return { ok: res.ok, data: d }; }); })
|
||||
.then(function(result) {
|
||||
if (!result.ok || result.data.error) {
|
||||
errBox.textContent = result.data.error || '{% if lang == 'de' %}Etwas ist schiefgelaufen. Bitte versuch es erneut.{% else %}Something went wrong. Please try again.{% endif %}';
|
||||
errBox.textContent = result.data.error || {{ t.sup_step4_error | tojson }};
|
||||
errBox.hidden = false;
|
||||
btn.disabled = false;
|
||||
btn.textContent = '{% if lang == 'de' %}Zur Kasse{% else %}Proceed to Checkout{% endif %}';
|
||||
btn.textContent = {{ t.sup_step4_checkout | tojson }};
|
||||
return;
|
||||
}
|
||||
Paddle.Checkout.open({
|
||||
@@ -130,13 +130,13 @@
|
||||
settings: result.data.settings
|
||||
});
|
||||
btn.disabled = false;
|
||||
btn.textContent = '{% if lang == 'de' %}Zur Kasse{% else %}Proceed to Checkout{% endif %}';
|
||||
btn.textContent = {{ t.sup_step4_checkout | tojson }};
|
||||
})
|
||||
.catch(function() {
|
||||
errBox.textContent = '{% if lang == 'de' %}Netzwerkfehler. Bitte überprüfe Deine Verbindung und versuch es erneut.{% else %}Network error. Please check your connection and try again.{% endif %}';
|
||||
errBox.textContent = {{ t.sup_step4_network_error | tojson }};
|
||||
errBox.hidden = false;
|
||||
btn.disabled = false;
|
||||
btn.textContent = '{% if lang == 'de' %}Zur Kasse{% else %}Proceed to Checkout{% endif %}';
|
||||
btn.textContent = {{ t.sup_step4_checkout | tojson }};
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{% if lang == 'de' %}Anbieter-Registrierung - {{ config.APP_NAME }}{% else %}Supplier Signup - {{ config.APP_NAME }}{% endif %}{% endblock %}
|
||||
{% block title %}{{ t.sup_signup_page_title }} - {{ config.APP_NAME }}{% endblock %}
|
||||
{% block paddle %}{% include "_paddle.html" %}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
@@ -119,7 +119,7 @@
|
||||
<div class="s-progress" id="s-progress">
|
||||
<div class="s-progress__meta">
|
||||
<span class="s-progress__label" id="s-progress-label">{{ t.sup_signup_step1 }}</span>
|
||||
<span class="s-progress__count" id="s-progress-count">1 {% if lang == 'de' %}von 4{% else %}of 4{% endif %}</span>
|
||||
<span class="s-progress__count" id="s-progress-count">1 {{ t.sup_signup_of_steps }}</span>
|
||||
</div>
|
||||
<div class="s-progress__track">
|
||||
<div class="s-progress__fill" id="s-progress-fill" style="width: 25%"></div>
|
||||
@@ -144,7 +144,7 @@ document.body.addEventListener('htmx:afterSwap', function(e) {
|
||||
var n = parseInt(step.dataset.step);
|
||||
var labels = [{{ t.sup_signup_step1 | tojson }}, {{ t.sup_signup_step2 | tojson }}, {{ t.sup_signup_step3 | tojson }}, {{ t.sup_signup_step4 | tojson }}];
|
||||
document.getElementById('s-progress-label').textContent = labels[n-1] || '';
|
||||
document.getElementById('s-progress-count').textContent = n + ' {% if lang == 'de' %}von 4{% else %}of 4{% endif %}';
|
||||
document.getElementById('s-progress-count').textContent = n + ' ' + {{ t.sup_signup_of_steps | tojson }};
|
||||
document.getElementById('s-progress-fill').style.width = (n * 25) + '%';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{% if lang == 'de' %}Willkommen! - {{ config.APP_NAME }}{% else %}Welcome! - {{ config.APP_NAME }}{% endif %}{% endblock %}
|
||||
{% block title %}{{ t.sup_success_page_title }} - {{ config.APP_NAME }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main style="background: linear-gradient(180deg, #F1F5F9, #F8FAFC); min-height: 80vh;">
|
||||
@@ -16,10 +16,10 @@
|
||||
<div style="background:#F8FAFC;border-radius:12px;padding:1rem;margin-bottom:1.5rem;text-align:left">
|
||||
<h3 style="font-size:0.8125rem;font-weight:700;margin:0 0 0.5rem">{{ t.sup_success_next_h3 }}</h3>
|
||||
<ul style="list-style:none;padding:0;margin:0;font-size:0.8125rem;color:#475569">
|
||||
<li style="padding:3px 0">✓ {% if lang == 'de' %}Dein Eintrag wird in wenigen Minuten aktualisiert{% else %}Your listing will be upgraded within minutes{% endif %}</li>
|
||||
<li style="padding:3px 0">✓ {% if lang == 'de' %}Lead-Credits wurden deinem Konto hinzugefügt{% else %}Lead credits have been added to your account{% endif %}</li>
|
||||
<li style="padding:3px 0">✓ {% if lang == 'de' %}Prüfe deine E-Mail auf einen Anmelde-Link{% else %}Check your email for a sign-in link{% endif %}</li>
|
||||
<li style="padding:3px 0">✓ {% if lang == 'de' %}Durchsuche und entsperre Leads in deinem Feed{% else %}Browse and unlock leads in your feed{% endif %}</li>
|
||||
<li style="padding:3px 0">✓ {{ t.sup_success_li1 }}</li>
|
||||
<li style="padding:3px 0">✓ {{ t.sup_success_li2 }}</li>
|
||||
<li style="padding:3px 0">✓ {{ t.sup_success_li3 }}</li>
|
||||
<li style="padding:3px 0">✓ {{ t.sup_success_li4 }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{% if lang == 'de' %}Anbieter-Warteliste - {{ config.APP_NAME }}{% else %}Supplier Waitlist - {{ config.APP_NAME }}{% endif %}{% endblock %}
|
||||
{% block title %}{{ t.sup_waitlist_page_title }} - {{ config.APP_NAME }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="container-page py-12">
|
||||
@@ -8,18 +8,17 @@
|
||||
{% set plan_info = plans.get(plan, plans['supplier_growth']) %}
|
||||
|
||||
<h1 class="text-2xl mb-1">{{ t.sup_waitlist_h1 }}</h1>
|
||||
<p class="text-slate mb-6">{% if lang == 'de' %}Wir bauen die ultimative Plattform, um verifizierte Padel-Anbieter mit Unternehmern zu verbinden. Sei Erster in der Schlange für den {{ plan_info.name }}-Tier-Zugang.{% else %}We're building the ultimate platform to connect verified padel suppliers with entrepreneurs. Be first in line for {{ plan_info.name }} tier access.{% endif %}</p>
|
||||
<p class="text-slate mb-6">{{ t.sup_waitlist_intro | tformat(plan_name=plan_info.name) }}</p>
|
||||
|
||||
<div class="bg-slate-50 border border-slate-200 rounded-lg p-4 mb-6">
|
||||
<h3 class="font-semibold text-navy text-sm mb-2">{% if lang == 'de' %}{{ plan_info.name }} Plan-Highlights{% else %}{{ plan_info.name }} Plan Highlights{% endif %}</h3>
|
||||
<h3 class="font-semibold text-navy text-sm mb-2">{{ t.sup_waitlist_plan_h3 | tformat(name=plan_info.name) }}</h3>
|
||||
<ul class="text-sm text-slate-dark space-y-1">
|
||||
{% set feature_list = plan_info.features_de if lang == 'de' else plan_info.features %}
|
||||
{% for feature in feature_list[:4] %}
|
||||
{% for key in plan_info.feature_keys[:4] %}
|
||||
<li class="flex items-start gap-2">
|
||||
<svg class="w-4 h-4 text-blue-600 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<span>{{ feature }}</span>
|
||||
<span>{{ t[key] }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
@@ -40,7 +39,7 @@
|
||||
required
|
||||
autofocus
|
||||
>
|
||||
<p class="form-hint">{% if lang == 'de' %}Frühzeitiger Zugang, exklusiver Launch-Preis und bevorzugtes Onboarding.{% else %}Get early access, exclusive launch pricing, and priority onboarding.{% endif %}</p>
|
||||
<p class="form-hint">{{ t.sup_waitlist_hint }}</p>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn w-full">{{ t.sup_waitlist_submit }}</button>
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{% if lang == 'de' %}Du stehst auf der Anbieter-Warteliste - {{ config.APP_NAME }}{% else %}You're on the Supplier Waitlist - {{ config.APP_NAME }}{% endif %}{% endblock %}
|
||||
{% block title %}{{ t.sup_waitlist_conf_page_title }} - {{ config.APP_NAME }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="container-page py-12">
|
||||
<div class="card max-w-sm mx-auto mt-8 text-center">
|
||||
{% set plan_info = plans.get(plan, plans['supplier_growth']) %}
|
||||
|
||||
<h1 class="text-2xl mb-4">{% if lang == 'de' %}Du stehst auf der Anbieter-Warteliste!{% else %}You're on the Supplier Waitlist!{% endif %}</h1>
|
||||
<h1 class="text-2xl mb-4">{{ t.sup_waitlist_conf_h1 }}</h1>
|
||||
|
||||
<p class="text-slate-dark">{% if lang == 'de' %}Wir haben eine Bestätigung gesendet an:{% else %}We've sent a confirmation to:{% endif %}</p>
|
||||
<p class="text-slate-dark">{{ t.sup_waitlist_conf_msg }}</p>
|
||||
<p class="font-semibold text-navy my-2">{{ email }}</p>
|
||||
|
||||
<p class="text-slate text-sm mb-2">{% if lang == 'de' %}Du gehörst zu den ersten Anbietern mit Zugang zum <strong>{{ plan_info.name }}</strong>-Tier bei unserem Launch.{% else %}You'll be among the first suppliers with access to the <strong>{{ plan_info.name }}</strong> tier when we launch.{% endif %}</p>
|
||||
<p class="text-slate text-sm mb-2">{{ t.sup_waitlist_conf_first_pre }}<strong>{{ plan_info.name }}</strong>{{ t.sup_waitlist_conf_first_post }}</p>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="text-left mt-6">
|
||||
<h3 class="text-sm font-semibold text-navy mb-3">{% if lang == 'de' %}Was du als Frühmitglied erhältst:{% else %}What you'll get as an early member:{% endif %}</h3>
|
||||
<h3 class="text-sm font-semibold text-navy mb-3">{{ t.sup_waitlist_conf_early_h3 }}</h3>
|
||||
<ul class="list-disc pl-6 space-y-1 text-sm text-slate-dark">
|
||||
<li>{% if lang == 'de' %}Erster Zugang zu qualifizierten Leads von Padel-Unternehmern{% else %}First access to qualified leads from padel entrepreneurs{% endif %}</li>
|
||||
<li>{% if lang == 'de' %}Exklusiver Launch-Preis (für 12 Monate festgeschrieben){% else %}Exclusive launch pricing (locked in for 12 months){% endif %}</li>
|
||||
<li>{% if lang == 'de' %}Vorrangiges Onboarding und Support bei der Eintragsoptimierung{% else %}Priority onboarding and listing optimization support{% endif %}</li>
|
||||
<li>{% if lang == 'de' %}Hervorgehobene Platzierung im Verzeichnis beim Launch{% else %}Featured placement in the directory at launch{% endif %}</li>
|
||||
<li>{{ t.sup_waitlist_conf_li1 }}</li>
|
||||
<li>{{ t.sup_waitlist_conf_li2 }}</li>
|
||||
<li>{{ t.sup_waitlist_conf_li3 }}</li>
|
||||
<li>{{ t.sup_waitlist_conf_li4 }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<a href="{{ url_for('directory.index') }}" class="btn-outline w-full mt-6">{% if lang == 'de' %}Anbieterverzeichnis durchsuchen{% else %}Browse Supplier Directory{% endif %}</a>
|
||||
<a href="{{ url_for('directory.index') }}" class="btn-outline w-full mt-6">{{ t.sup_waitlist_conf_btn }}</a>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="{{ s.lang }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>{{ css }}</style>
|
||||
@@ -10,51 +10,47 @@
|
||||
<h1>{{ s.title }}</h1>
|
||||
<div class="subtitle">{{ s.subtitle }}</div>
|
||||
{% if s.scenario_name %}
|
||||
<p class="meta">Scenario: {{ s.scenario_name }}{% if s.location %} — {{ s.location }}{% endif %}</p>
|
||||
<p class="meta">{{ s.labels.scenario }}: {{ s.scenario_name }}{% if s.location %} — {{ s.location }}{% endif %}</p>
|
||||
{% endif %}
|
||||
<p class="meta">{{ s.courts }}</p>
|
||||
<p class="meta">Generated by Padelnomics — padelnomics.io</p>
|
||||
<p class="meta">{{ s.labels.generated_by }}</p>
|
||||
|
||||
<!-- Executive Summary -->
|
||||
<h2>{{ s.executive_summary.heading }}</h2>
|
||||
<div class="summary-grid">
|
||||
<div class="summary-card">
|
||||
<div class="label">Total Investment</div>
|
||||
<div class="label">{{ s.labels.total_investment }}</div>
|
||||
<div class="value">{{ s.executive_summary.total_capex }}</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="label">Equity Required</div>
|
||||
<div class="label">{{ s.labels.equity_required }}</div>
|
||||
<div class="value">{{ s.executive_summary.equity }}</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="label">Year 3 EBITDA</div>
|
||||
<div class="label">{{ s.labels.year3_ebitda }}</div>
|
||||
<div class="value">{{ s.executive_summary.y3_ebitda }}</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="label">IRR</div>
|
||||
<div class="label">{{ s.labels.irr }}</div>
|
||||
<div class="value value--blue">{{ s.executive_summary.irr }}</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="label">Payback Period</div>
|
||||
<div class="label">{{ s.labels.payback_period }}</div>
|
||||
<div class="value">{{ s.executive_summary.payback }}</div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="label">Year 1 Revenue</div>
|
||||
<div class="label">{{ s.labels.year1_revenue }}</div>
|
||||
<div class="value">{{ s.executive_summary.y1_revenue }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>This business plan models a <strong>{{ s.executive_summary.facility_type }}</strong> padel facility with
|
||||
<strong>{{ s.executive_summary.courts }} courts</strong> ({{ s.executive_summary.sqm }} m²).
|
||||
Total investment is {{ s.executive_summary.total_capex }}, financed with {{ s.executive_summary.equity }} equity
|
||||
and {{ s.executive_summary.loan }} debt. The projected IRR is {{ s.executive_summary.irr }} with a payback period
|
||||
of {{ s.executive_summary.payback }}.</p>
|
||||
<p>{{ s.labels.exec_paragraph }}</p>
|
||||
|
||||
<!-- Investment Plan (CAPEX) -->
|
||||
<h2>{{ s.investment.heading }}</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Item</th><th style="text-align:right">Amount</th><th>Notes</th></tr>
|
||||
<tr><th>{{ s.labels.item }}</th><th style="text-align:right">{{ s.labels.amount }}</th><th>{{ s.labels.notes }}</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in s.investment.items %}
|
||||
@@ -65,15 +61,13 @@ of {{ s.executive_summary.payback }}.</p>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr class="total-row">
|
||||
<td>Total CAPEX</td>
|
||||
<td>{{ s.labels.total_capex }}</td>
|
||||
<td style="text-align:right">{{ s.investment.total }}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p style="font-size:9pt;color:#64748B">
|
||||
CAPEX per court: {{ s.investment.per_court }} • CAPEX per m²: {{ s.investment.per_sqm }}
|
||||
</p>
|
||||
<p style="font-size:9pt;color:#64748B">{{ s.labels.capex_stats }}</p>
|
||||
|
||||
<!-- Financing Structure -->
|
||||
<h2>{{ s.financing.heading }}</h2>
|
||||
@@ -83,13 +77,13 @@ of {{ s.executive_summary.payback }}.</p>
|
||||
</div>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr><td>Equity</td><td style="text-align:right">{{ s.financing.equity }}</td></tr>
|
||||
<tr><td>Loan ({{ s.financing.loan_pct }})</td><td style="text-align:right">{{ s.financing.loan }}</td></tr>
|
||||
<tr><td>Interest Rate</td><td style="text-align:right">{{ s.financing.interest_rate }}</td></tr>
|
||||
<tr><td>Loan Term</td><td style="text-align:right">{{ s.financing.term }}</td></tr>
|
||||
<tr><td>Monthly Payment</td><td style="text-align:right">{{ s.financing.monthly_payment }}</td></tr>
|
||||
<tr><td>Annual Debt Service</td><td style="text-align:right">{{ s.financing.annual_debt_service }}</td></tr>
|
||||
<tr><td>Loan-to-Value</td><td style="text-align:right">{{ s.financing.ltv }}</td></tr>
|
||||
<tr><td>{{ s.labels.equity }}</td><td style="text-align:right">{{ s.financing.equity }}</td></tr>
|
||||
<tr><td>{{ s.labels.loan }} ({{ s.financing.loan_pct }})</td><td style="text-align:right">{{ s.financing.loan }}</td></tr>
|
||||
<tr><td>{{ s.labels.interest_rate }}</td><td style="text-align:right">{{ s.financing.interest_rate }}</td></tr>
|
||||
<tr><td>{{ s.labels.loan_term }}</td><td style="text-align:right">{{ s.financing.term }}</td></tr>
|
||||
<tr><td>{{ s.labels.monthly_payment }}</td><td style="text-align:right">{{ s.financing.monthly_payment }}</td></tr>
|
||||
<tr><td>{{ s.labels.annual_debt_service }}</td><td style="text-align:right">{{ s.financing.annual_debt_service }}</td></tr>
|
||||
<tr><td>{{ s.labels.ltv }}</td><td style="text-align:right">{{ s.financing.ltv }}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -97,7 +91,7 @@ of {{ s.executive_summary.payback }}.</p>
|
||||
<h2>{{ s.operations.heading }}</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Item</th><th style="text-align:right">Monthly</th><th>Notes</th></tr>
|
||||
<tr><th>{{ s.labels.item }}</th><th style="text-align:right">{{ s.labels.monthly }}</th><th>{{ s.labels.notes }}</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in s.operations.items %}
|
||||
@@ -108,24 +102,24 @@ of {{ s.executive_summary.payback }}.</p>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr class="total-row">
|
||||
<td>Total Monthly OPEX</td>
|
||||
<td>{{ s.labels.total_monthly_opex }}</td>
|
||||
<td style="text-align:right">{{ s.operations.monthly_total }}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p style="font-size:9pt;color:#64748B">Annual OPEX: {{ s.operations.annual_total }}</p>
|
||||
<p style="font-size:9pt;color:#64748B">{{ s.labels.annual_opex }}: {{ s.operations.annual_total }}</p>
|
||||
|
||||
<!-- Revenue Model -->
|
||||
<h2>{{ s.revenue.heading }}</h2>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr><td>Weighted Hourly Rate</td><td style="text-align:right">{{ s.revenue.weighted_rate }}</td></tr>
|
||||
<tr><td>Target Utilization</td><td style="text-align:right">{{ s.revenue.utilization }}</td></tr>
|
||||
<tr><td>Gross Monthly Revenue</td><td style="text-align:right">{{ s.revenue.gross_monthly }}</td></tr>
|
||||
<tr><td>Net Monthly Revenue</td><td style="text-align:right">{{ s.revenue.net_monthly }}</td></tr>
|
||||
<tr><td>Monthly EBITDA</td><td style="text-align:right">{{ s.revenue.ebitda_monthly }}</td></tr>
|
||||
<tr><td>Monthly Net Cash Flow</td><td style="text-align:right">{{ s.revenue.net_cf_monthly }}</td></tr>
|
||||
<tr><td>{{ s.labels.weighted_hourly_rate }}</td><td style="text-align:right">{{ s.revenue.weighted_rate }}</td></tr>
|
||||
<tr><td>{{ s.labels.target_utilization }}</td><td style="text-align:right">{{ s.revenue.utilization }}</td></tr>
|
||||
<tr><td>{{ s.labels.gross_monthly_revenue }}</td><td style="text-align:right">{{ s.revenue.gross_monthly }}</td></tr>
|
||||
<tr><td>{{ s.labels.net_monthly_revenue }}</td><td style="text-align:right">{{ s.revenue.net_monthly }}</td></tr>
|
||||
<tr><td>{{ s.labels.monthly_ebitda }}</td><td style="text-align:right">{{ s.revenue.ebitda_monthly }}</td></tr>
|
||||
<tr><td>{{ s.labels.monthly_net_cf }}</td><td style="text-align:right">{{ s.revenue.net_cf_monthly }}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -133,12 +127,12 @@ of {{ s.executive_summary.payback }}.</p>
|
||||
<h2>{{ s.annuals.heading }}</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Year</th><th style="text-align:right">Revenue</th><th style="text-align:right">EBITDA</th><th style="text-align:right">Debt Service</th><th style="text-align:right">Net CF</th></tr>
|
||||
<tr><th>{{ s.labels.year }}</th><th style="text-align:right">{{ s.labels.revenue }}</th><th style="text-align:right">{{ s.labels.ebitda }}</th><th style="text-align:right">{{ s.labels.debt_service }}</th><th style="text-align:right">{{ s.labels.net_cf }}</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for yr in s.annuals.years %}
|
||||
<tr>
|
||||
<td>Year {{ yr.year }}</td>
|
||||
<td>{{ s.labels.year }} {{ yr.year }}</td>
|
||||
<td style="text-align:right">{{ yr.revenue }}</td>
|
||||
<td style="text-align:right">{{ yr.ebitda }}</td>
|
||||
<td style="text-align:right">{{ yr.debt_service }}</td>
|
||||
@@ -152,35 +146,35 @@ of {{ s.executive_summary.payback }}.</p>
|
||||
<h2>{{ s.metrics.heading }}</h2>
|
||||
<div class="metrics-grid">
|
||||
<div class="metric-box">
|
||||
<div class="label">IRR</div>
|
||||
<div class="label">{{ s.labels.irr }}</div>
|
||||
<div class="value">{{ s.metrics.irr }}</div>
|
||||
</div>
|
||||
<div class="metric-box">
|
||||
<div class="label">MOIC</div>
|
||||
<div class="label">{{ s.labels.moic }}</div>
|
||||
<div class="value">{{ s.metrics.moic }}</div>
|
||||
</div>
|
||||
<div class="metric-box">
|
||||
<div class="label">Cash-on-Cash (Y3)</div>
|
||||
<div class="label">{{ s.labels.cash_on_cash }}</div>
|
||||
<div class="value">{{ s.metrics.cash_on_cash }}</div>
|
||||
</div>
|
||||
<div class="metric-box">
|
||||
<div class="label">Payback</div>
|
||||
<div class="label">{{ s.labels.payback }}</div>
|
||||
<div class="value">{{ s.metrics.payback }}</div>
|
||||
</div>
|
||||
<div class="metric-box">
|
||||
<div class="label">Break-Even Util.</div>
|
||||
<div class="label">{{ s.labels.break_even_util }}</div>
|
||||
<div class="value">{{ s.metrics.break_even_util }}</div>
|
||||
</div>
|
||||
<div class="metric-box">
|
||||
<div class="label">EBITDA Margin</div>
|
||||
<div class="label">{{ s.labels.ebitda_margin }}</div>
|
||||
<div class="value">{{ s.metrics.ebitda_margin }}</div>
|
||||
</div>
|
||||
<div class="metric-box">
|
||||
<div class="label">DSCR (Y3)</div>
|
||||
<div class="label">{{ s.labels.dscr_y3 }}</div>
|
||||
<div class="value">{{ s.metrics.dscr_y3 }}</div>
|
||||
</div>
|
||||
<div class="metric-box">
|
||||
<div class="label">Yield on Cost</div>
|
||||
<div class="label">{{ s.labels.yield_on_cost }}</div>
|
||||
<div class="value">{{ s.metrics.yield_on_cost }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -189,7 +183,7 @@ of {{ s.executive_summary.payback }}.</p>
|
||||
<h2>{{ s.cashflow_12m.heading }}</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Month</th><th style="text-align:right">Revenue</th><th style="text-align:right">OPEX</th><th style="text-align:right">EBITDA</th><th style="text-align:right">Debt</th><th style="text-align:right">Net CF</th><th style="text-align:right">Cumulative</th></tr>
|
||||
<tr><th>{{ s.labels.month }}</th><th style="text-align:right">{{ s.labels.revenue }}</th><th style="text-align:right">{{ s.labels.opex }}</th><th style="text-align:right">{{ s.labels.ebitda }}</th><th style="text-align:right">{{ s.labels.debt }}</th><th style="text-align:right">{{ s.labels.net_cf }}</th><th style="text-align:right">{{ s.labels.cumulative }}</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for m in s.cashflow_12m.months %}
|
||||
@@ -208,10 +202,7 @@ of {{ s.executive_summary.payback }}.</p>
|
||||
|
||||
<!-- Disclaimer -->
|
||||
<div class="disclaimer">
|
||||
<strong>Disclaimer:</strong> This business plan is generated from user-provided assumptions using the Padelnomics
|
||||
financial model. All projections are estimates and do not constitute financial advice. Actual results may vary
|
||||
significantly based on market conditions, execution, and other factors. Consult with financial advisors before
|
||||
making investment decisions. © Padelnomics — padelnomics.io
|
||||
{{ s.labels.disclaimer }}
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -386,7 +386,7 @@ class TestBakeScenarioCards:
|
||||
await _create_published_scenario(slug="cta-test")
|
||||
html = "[scenario:cta-test]"
|
||||
result = await bake_scenario_cards(html)
|
||||
assert "/planner/" in result
|
||||
assert "planner" in result
|
||||
assert "Try with your own numbers" in result
|
||||
|
||||
async def test_invalid_section_ignored(self, db):
|
||||
|
||||
113
padelnomics/tests/test_i18n_parity.py
Normal file
113
padelnomics/tests/test_i18n_parity.py
Normal file
@@ -0,0 +1,113 @@
|
||||
"""
|
||||
Tests that EN and DE locale files are consistent.
|
||||
"""
|
||||
import pytest
|
||||
from padelnomics.i18n import _TRANSLATIONS, _CALC_ITEM_NAMES, SUPPORTED_LANGS
|
||||
|
||||
# Keys with identical EN/DE values are acceptable — financial abbreviations,
|
||||
# month abbreviations, English brand names used in German, and UI chrome
|
||||
# that intentionally stays English in DE (Dashboard, Admin, Feedback).
|
||||
_IDENTICAL_VALUE_ALLOWLIST = {
|
||||
# Financial / technical abbreviations (same in DE)
|
||||
"card_cash_on_cash", "card_irr", "card_moic", "card_rev_pah",
|
||||
"th_dscr", "th_ebitda", "wf_moic", "wiz_capex", "wiz_irr",
|
||||
# Month abbreviations (same in DE: Jan, Feb, Apr, Jun, Jul, Aug, Sep, Nov)
|
||||
"month_jan", "month_feb", "month_apr", "month_jun",
|
||||
"month_jul", "month_aug", "month_sep", "month_nov",
|
||||
# Indoor / Outdoor (English terms used in DE)
|
||||
"label_indoor", "label_outdoor", "toggle_indoor", "toggle_outdoor",
|
||||
"features_card_3_h2", "landing_feature_3_h3",
|
||||
"q1_facility_indoor", "q1_facility_outdoor", "q1_facility_both",
|
||||
# Revenue streams and product labels (English terms used in DE)
|
||||
"stream_coaching", "stream_fb",
|
||||
"pill_light_led_standard", "q1_lighting_led_std",
|
||||
# Plan names (brand names, same in DE)
|
||||
"sup_basic_name", "sup_growth_name", "sup_pro_name",
|
||||
"suppliers_basic_name", "suppliers_growth_name", "suppliers_pro_name",
|
||||
"dir_card_featured", "dir_card_growth", "dir_cat_software",
|
||||
# Supplier boost option names
|
||||
"sup_boost_logo", "sup_boost_sticky", "sup_boosts_h3",
|
||||
# Comparison table headers that use English terms in DE
|
||||
"sup_cmp_th_ads", "sup_cmp_th_us",
|
||||
# Problem section heading — "Google Ads" is a brand name
|
||||
"sup_prob_ads_h3", "suppliers_problem_2_h3",
|
||||
# Budget/lead labels that are English in DE
|
||||
"sup_lead_budget", "suppliers_leads_budget",
|
||||
# UI chrome that stays English in DE
|
||||
"nav_admin", "nav_dashboard", "nav_feedback",
|
||||
# Country names that are the same
|
||||
"country_uk", "country_us",
|
||||
"dir_country_CN", "dir_country_PT", "dir_country_TH",
|
||||
# Optional annotation
|
||||
"q9_company_note",
|
||||
# Dashboard chrome that stays English in DE (brand term)
|
||||
"dash_page_title", "dash_h1", "dash_name_label",
|
||||
# Supplier dashboard — analytics brand name
|
||||
"sd_ov_via_umami",
|
||||
# Lead heat filter labels (English terms used in DE)
|
||||
"sd_leads_filter_hot", "sd_leads_filter_warm", "sd_leads_filter_cool",
|
||||
# "Budget", "Name", "Phase", "Investor" — same in both languages
|
||||
"sd_leads_budget", "sd_card_budget", "sd_unlocked_label_budget",
|
||||
"sd_unlocked_label_name", "sd_unlocked_label_phase", "sd_stakeholder_investor",
|
||||
# Listing form labels that are English brand terms / same in DE
|
||||
"sd_lst_logo", "sd_lst_website",
|
||||
# Boost option name — "Logo" is the same in DE
|
||||
"sd_boost_logo_name",
|
||||
# Business plan — Indoor/Outdoor same in DE, financial abbreviations
|
||||
"bp_indoor", "bp_outdoor",
|
||||
"bp_lbl_ebitda", "bp_lbl_irr", "bp_lbl_moic", "bp_lbl_opex",
|
||||
}
|
||||
|
||||
|
||||
def test_en_de_key_parity():
|
||||
"""EN and DE must have identical key sets."""
|
||||
en_keys = set(_TRANSLATIONS["en"].keys())
|
||||
de_keys = set(_TRANSLATIONS["de"].keys())
|
||||
en_only = en_keys - de_keys
|
||||
de_only = de_keys - en_keys
|
||||
assert not en_only, f"Keys in EN but not DE: {sorted(en_only)}"
|
||||
assert not de_only, f"Keys in DE but not EN: {sorted(de_only)}"
|
||||
|
||||
|
||||
def test_all_values_non_empty():
|
||||
"""No translation value should be an empty string."""
|
||||
for lang in SUPPORTED_LANGS:
|
||||
for key, value in _TRANSLATIONS[lang].items():
|
||||
assert value, f"Empty value for key {key!r} in lang {lang!r}"
|
||||
|
||||
|
||||
def test_no_untranslated_copy_paste():
|
||||
"""No key should have an identical EN and DE value (catches missed translations).
|
||||
|
||||
Some keys are exempt — see _IDENTICAL_VALUE_ALLOWLIST.
|
||||
"""
|
||||
en = _TRANSLATIONS["en"]
|
||||
de = _TRANSLATIONS["de"]
|
||||
violations = []
|
||||
for key in en:
|
||||
if key in _IDENTICAL_VALUE_ALLOWLIST:
|
||||
continue
|
||||
if en[key] == de[key]:
|
||||
violations.append((key, en[key]))
|
||||
assert not violations, (
|
||||
f"Keys with identical EN/DE values (likely untranslated): "
|
||||
+ ", ".join(f"{k!r}={v!r}" for k, v in violations[:10])
|
||||
+ (f" ... and {len(violations) - 10} more" if len(violations) > 10 else "")
|
||||
)
|
||||
|
||||
|
||||
def test_calc_item_names_key_parity():
|
||||
"""EN and DE calc item names must have identical key sets."""
|
||||
en_keys = set(_CALC_ITEM_NAMES["en"].keys())
|
||||
de_keys = set(_CALC_ITEM_NAMES["de"].keys())
|
||||
assert en_keys == de_keys, (
|
||||
f"Calc item key mismatch — "
|
||||
f"EN-only: {sorted(en_keys - de_keys)}, DE-only: {sorted(de_keys - en_keys)}"
|
||||
)
|
||||
|
||||
|
||||
def test_calc_item_names_non_empty():
|
||||
"""No calc item name should be empty."""
|
||||
for lang in SUPPORTED_LANGS:
|
||||
for key, value in _CALC_ITEM_NAMES[lang].items():
|
||||
assert value, f"Empty calc item name for key {key!r} in lang {lang!r}"
|
||||
Reference in New Issue
Block a user