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(",", ".")
|
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:
|
def create_app() -> Quart:
|
||||||
"""Create and configure the Quart application."""
|
"""Create and configure the Quart application."""
|
||||||
|
|
||||||
@@ -67,12 +76,13 @@ def create_app() -> Quart:
|
|||||||
|
|
||||||
app.secret_key = config.SECRET_KEY
|
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_currency"] = _fmt_currency
|
||||||
app.jinja_env.filters["fmt_k"] = _fmt_k
|
app.jinja_env.filters["fmt_k"] = _fmt_k
|
||||||
app.jinja_env.filters["fmt_pct"] = _fmt_pct
|
app.jinja_env.filters["fmt_pct"] = _fmt_pct
|
||||||
app.jinja_env.filters["fmt_x"] = _fmt_x
|
app.jinja_env.filters["fmt_x"] = _fmt_x
|
||||||
app.jinja_env.filters["fmt_n"] = _fmt_n
|
app.jinja_env.filters["fmt_n"] = _fmt_n
|
||||||
|
app.jinja_env.filters["tformat"] = _tformat # translate with placeholders: {{ t.key | tformat(count=n) }}
|
||||||
|
|
||||||
# Session config
|
# Session config
|
||||||
app.config["SESSION_COOKIE_SECURE"] = not config.DEBUG
|
app.config["SESSION_COOKIE_SECURE"] = not config.DEBUG
|
||||||
@@ -186,7 +196,8 @@ def create_app() -> Quart:
|
|||||||
@app.context_processor
|
@app.context_processor
|
||||||
def inject_globals():
|
def inject_globals():
|
||||||
from datetime import datetime
|
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"
|
effective_lang = lang if lang in SUPPORTED_LANGS else "en"
|
||||||
return {
|
return {
|
||||||
"config": config,
|
"config": config,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from quart import Blueprint, flash, g, jsonify, redirect, render_template, reque
|
|||||||
|
|
||||||
from ..auth.routes import login_required
|
from ..auth.routes import login_required
|
||||||
from ..core import config, execute, fetch_one, get_paddle_price
|
from ..core import config, execute, fetch_one, get_paddle_price
|
||||||
|
from ..i18n import get_translations
|
||||||
|
|
||||||
|
|
||||||
def _paddle_client() -> PaddleClient:
|
def _paddle_client() -> PaddleClient:
|
||||||
@@ -177,7 +178,8 @@ async def manage():
|
|||||||
"""Redirect to Paddle customer portal."""
|
"""Redirect to Paddle customer portal."""
|
||||||
sub = await get_subscription(g.user["id"])
|
sub = await get_subscription(g.user["id"])
|
||||||
if not sub or not sub.get("provider_subscription_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"))
|
return redirect(url_for("dashboard.settings"))
|
||||||
|
|
||||||
paddle = _paddle_client()
|
paddle = _paddle_client()
|
||||||
|
|||||||
@@ -1,51 +1,51 @@
|
|||||||
{% extends "base.html" %}
|
{% 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 %}
|
{% 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 name="description" content="{{ t.billing_pricing_meta_desc }}">
|
||||||
<meta property="og:title" content="Free Padel Court Financial Planner - {{ config.APP_NAME }}">
|
<meta property="og:title" content="{{ t.billing_pricing_og_title }} - {{ 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 property="og:description" content="{{ t.billing_pricing_og_desc }}">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main class="container-page py-12">
|
<main class="container-page py-12">
|
||||||
<div class="heading-group text-center">
|
<div class="heading-group text-center">
|
||||||
<h1 class="text-3xl">100% Free. No Catch.</h1>
|
<h1 class="text-3xl">{{ t.billing_pricing_h1 }}</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>
|
<p>{{ t.billing_pricing_subtitle }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid-2 mt-8">
|
<div class="grid-2 mt-8">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<p class="card-header">Financial Planner</p>
|
<p class="card-header">{{ t.billing_planner_card }}</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="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">
|
<ul class="space-y-2 text-sm text-slate-dark mb-6">
|
||||||
<li>60+ adjustable variables</li>
|
<li>{{ t.billing_feature_1 }}</li>
|
||||||
<li>6 analysis tabs (CAPEX, Operating, Cash Flow, Returns, Metrics)</li>
|
<li>{{ t.billing_feature_2 }}</li>
|
||||||
<li>Sensitivity analysis (utilization + pricing)</li>
|
<li>{{ t.billing_feature_3 }}</li>
|
||||||
<li>Save unlimited scenarios</li>
|
<li>{{ t.billing_feature_4 }}</li>
|
||||||
<li>Interactive charts</li>
|
<li>{{ t.billing_feature_5 }}</li>
|
||||||
<li>Indoor/outdoor & rent/buy models</li>
|
<li>{{ t.billing_feature_6 }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
{% if user %}
|
{% 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 %}
|
{% 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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<p class="card-header">Need Help Building?</p>
|
<p class="card-header">{{ t.billing_help_card }}</p>
|
||||||
<p class="text-slate-dark mb-4">We connect you with verified partners</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">
|
<ul class="space-y-2 text-sm text-slate-dark mb-6">
|
||||||
<li>Court supplier quotes</li>
|
<li>{{ t.billing_help_feature_1 }}</li>
|
||||||
<li>Financing & bank connections</li>
|
<li>{{ t.billing_help_feature_2 }}</li>
|
||||||
<li>Construction planning</li>
|
<li>{{ t.billing_help_feature_3 }}</li>
|
||||||
<li>Equipment sourcing</li>
|
<li>{{ t.billing_help_feature_4 }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
{% if user %}
|
{% 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 %}
|
{% 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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Welcome - {{ config.APP_NAME }}{% endblock %}
|
{% block title %}{{ t.billing_success_title }} - {{ config.APP_NAME }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main class="container-page py-12">
|
<main class="container-page py-12">
|
||||||
<div class="card max-w-md mx-auto mt-8 text-center">
|
<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>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import json
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from .core import fetch_one
|
from .core import fetch_one
|
||||||
|
from .i18n import get_translations
|
||||||
from .planner.calculator import calc, validate_state
|
from .planner.calculator import calc, validate_state
|
||||||
|
|
||||||
TEMPLATE_DIR = Path(__file__).parent / "templates" / "businessplan"
|
TEMPLATE_DIR = Path(__file__).parent / "templates" / "businessplan"
|
||||||
@@ -27,57 +28,66 @@ def _fmt_pct(n) -> str:
|
|||||||
return f"{n * 100:.1f}%"
|
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."""
|
"""Format payback month index as readable string."""
|
||||||
if idx < 0:
|
if idx < 0:
|
||||||
return "Not reached in 60 months"
|
return t["bp_payback_not_reached"]
|
||||||
months = idx + 1
|
months = idx + 1
|
||||||
if months <= 12:
|
if months <= 12:
|
||||||
return f"{months} months"
|
return t["bp_months"].format(n=months)
|
||||||
years = months / 12
|
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:
|
def get_plan_sections(state: dict, d: dict, language: str = "en") -> dict:
|
||||||
"""Extract and format all business plan sections from planner data."""
|
"""Extract and format all business plan sections from planner data."""
|
||||||
s = state
|
s = state
|
||||||
is_en = language == "en"
|
t = get_translations(language)
|
||||||
|
|
||||||
venue_type = "Indoor" if s["venue"] == "indoor" else "Outdoor"
|
venue_type = t["bp_indoor"] if s["venue"] == "indoor" else t["bp_outdoor"]
|
||||||
own_type = "Own" if s["own"] == "buy" else "Rent"
|
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 = {
|
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')}",
|
"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
|
||||||
"executive_summary": {
|
"executive_summary": {
|
||||||
"heading": "Executive Summary" if is_en else "Zusammenfassung",
|
"heading": t["bp_exec_summary"],
|
||||||
"facility_type": f"{venue_type} ({own_type})",
|
"facility_type": f"{venue_type} ({own_type})",
|
||||||
"courts": d["totalCourts"],
|
"courts": d["totalCourts"],
|
||||||
"sqm": d["sqm"],
|
"sqm": d["sqm"],
|
||||||
"total_capex": _fmt_eur(d["capex"]),
|
"total_capex": total_capex_str,
|
||||||
"equity": _fmt_eur(d["equity"]),
|
"equity": equity_str,
|
||||||
"loan": _fmt_eur(d["loanAmount"]),
|
"loan": loan_str,
|
||||||
"y1_revenue": _fmt_eur(d["annuals"][0]["revenue"]) if d["annuals"] else "-",
|
"y1_revenue": _fmt_eur(d["annuals"][0]["revenue"]) if d["annuals"] else "-",
|
||||||
"y3_ebitda": _fmt_eur(d["stabEbitda"]),
|
"y3_ebitda": _fmt_eur(d["stabEbitda"]),
|
||||||
"irr": _fmt_pct(d["irr"]),
|
"irr": irr_str,
|
||||||
"payback": _fmt_months(d["paybackIdx"]),
|
"payback": payback_str,
|
||||||
},
|
},
|
||||||
|
|
||||||
# Investment Plan (CAPEX)
|
# Investment Plan (CAPEX)
|
||||||
"investment": {
|
"investment": {
|
||||||
"heading": "Investment Plan" if is_en else "Investitionsplan",
|
"heading": t["bp_investment"],
|
||||||
"items": d["capexItems"],
|
"items": d["capexItems"],
|
||||||
"total": _fmt_eur(d["capex"]),
|
"total": total_capex_str,
|
||||||
"per_court": _fmt_eur(d["capexPerCourt"]),
|
"per_court": per_court_str,
|
||||||
"per_sqm": _fmt_eur(d["capexPerSqm"]),
|
"per_sqm": per_sqm_str,
|
||||||
},
|
},
|
||||||
|
|
||||||
# Operating Costs
|
# Operating Costs
|
||||||
"operations": {
|
"operations": {
|
||||||
"heading": "Operating Costs" if is_en else "Betriebskosten",
|
"heading": t["bp_operations"],
|
||||||
"items": d["opexItems"],
|
"items": d["opexItems"],
|
||||||
"monthly_total": _fmt_eur(d["opex"]),
|
"monthly_total": _fmt_eur(d["opex"]),
|
||||||
"annual_total": _fmt_eur(d["annualOpex"]),
|
"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 Model
|
||||||
"revenue": {
|
"revenue": {
|
||||||
"heading": "Revenue & Profitability" if is_en else "Umsatz & Rentabilit\u00e4t",
|
"heading": t["bp_revenue"],
|
||||||
"weighted_rate": _fmt_eur(d["weightedRate"]),
|
"weighted_rate": _fmt_eur(d["weightedRate"]),
|
||||||
"utilization": _fmt_pct(s["utilTarget"] / 100),
|
"utilization": _fmt_pct(s["utilTarget"] / 100),
|
||||||
"gross_monthly": _fmt_eur(d["grossRevMonth"]),
|
"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
|
# 5-Year P&L
|
||||||
"annuals": {
|
"annuals": {
|
||||||
"heading": "5-Year Projection" if is_en else "5-Jahres-Projektion",
|
"heading": t["bp_annuals"],
|
||||||
"years": [
|
"years": [
|
||||||
{
|
{
|
||||||
"year": a["year"],
|
"year": a["year"],
|
||||||
@@ -111,12 +121,12 @@ def get_plan_sections(state: dict, d: dict, language: str = "en") -> dict:
|
|||||||
|
|
||||||
# Financing
|
# Financing
|
||||||
"financing": {
|
"financing": {
|
||||||
"heading": "Financing Structure" if is_en else "Finanzierungsstruktur",
|
"heading": t["bp_financing"],
|
||||||
"loan_pct": _fmt_pct(s["loanPct"] / 100),
|
"loan_pct": _fmt_pct(s["loanPct"] / 100),
|
||||||
"equity": _fmt_eur(d["equity"]),
|
"equity": equity_str,
|
||||||
"loan": _fmt_eur(d["loanAmount"]),
|
"loan": loan_str,
|
||||||
"interest_rate": f"{s['interestRate']}%",
|
"interest_rate": f"{s['interestRate']}%",
|
||||||
"term": f"{s['loanTerm']} years",
|
"term": t["bp_years"].format(n=s["loanTerm"]),
|
||||||
"monthly_payment": _fmt_eur(d["monthlyPayment"]),
|
"monthly_payment": _fmt_eur(d["monthlyPayment"]),
|
||||||
"annual_debt_service": _fmt_eur(d["annualDebtService"]),
|
"annual_debt_service": _fmt_eur(d["annualDebtService"]),
|
||||||
"ltv": _fmt_pct(d["ltv"]),
|
"ltv": _fmt_pct(d["ltv"]),
|
||||||
@@ -124,11 +134,11 @@ def get_plan_sections(state: dict, d: dict, language: str = "en") -> dict:
|
|||||||
|
|
||||||
# Key Metrics
|
# Key Metrics
|
||||||
"metrics": {
|
"metrics": {
|
||||||
"heading": "Key Metrics" if is_en else "Kennzahlen",
|
"heading": t["bp_metrics"],
|
||||||
"irr": _fmt_pct(d["irr"]),
|
"irr": irr_str,
|
||||||
"moic": f"{d['moic']:.2f}x",
|
"moic": f"{d['moic']:.2f}x",
|
||||||
"cash_on_cash": _fmt_pct(d["cashOnCash"]),
|
"cash_on_cash": _fmt_pct(d["cashOnCash"]),
|
||||||
"payback": _fmt_months(d["paybackIdx"]),
|
"payback": payback_str,
|
||||||
"break_even_util": _fmt_pct(d["breakEvenUtil"]),
|
"break_even_util": _fmt_pct(d["breakEvenUtil"]),
|
||||||
"ebitda_margin": _fmt_pct(d["ebitdaMargin"]),
|
"ebitda_margin": _fmt_pct(d["ebitdaMargin"]),
|
||||||
"dscr_y3": f"{d['dscr'][2]['dscr']:.2f}x" if len(d["dscr"]) >= 3 else "-",
|
"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
|
# 12-Month Cash Flow
|
||||||
"cashflow_12m": {
|
"cashflow_12m": {
|
||||||
"heading": "12-Month Cash Flow" if is_en else "12-Monats-Liquidit\u00e4tsplan",
|
"heading": t["bp_cashflow_12m"],
|
||||||
"months": [
|
"months": [
|
||||||
{
|
{
|
||||||
"month": m["m"],
|
"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]
|
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
|
return sections
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from markupsafe import Markup
|
|||||||
from quart import Blueprint, abort, g, render_template, request
|
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 ..core import capture_waitlist_email, config, csrf_protect, fetch_all, fetch_one, waitlist_gate
|
||||||
|
from ..i18n import get_translations
|
||||||
|
|
||||||
bp = Blueprint(
|
bp = Blueprint(
|
||||||
"content",
|
"content",
|
||||||
@@ -37,9 +38,20 @@ SECTION_TEMPLATES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Standalone Jinja2 env for baking scenario cards into static HTML.
|
# 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"
|
_TEMPLATE_DIR = Path(__file__).parent / "templates"
|
||||||
_bake_env = Environment(loader=FileSystemLoader(str(_TEMPLATE_DIR)), autoescape=True)
|
_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:
|
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"])
|
state_data = json.loads(scenario["state_json"])
|
||||||
|
|
||||||
tmpl = _bake_env.get_template(template_name)
|
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():]
|
html = html[:match.start()] + card_html + html[match.end():]
|
||||||
|
|
||||||
return html
|
return html
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
<header class="mb-8">
|
<header class="mb-8">
|
||||||
<h1 class="text-3xl mb-2">{{ article.title }}</h1>
|
<h1 class="text-3xl mb-2">{{ article.title }}</h1>
|
||||||
<p class="text-sm text-slate">
|
<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>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{% extends "base.html" %}
|
{% 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 %}
|
{% 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 name="description" content="{{ t.markets_page_description }}">
|
||||||
<meta property="og:title" content="{% if lang == 'de' %}Padel-Märkte - {{ config.APP_NAME }}{% else %}Padel Markets - {{ config.APP_NAME }}{% endif %}">
|
<meta property="og:title" content="{{ t.markets_page_og_title }} - {{ config.APP_NAME }}">
|
||||||
<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 property="og:description" content="{{ t.markets_page_og_description }}">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
<div class="card mb-8">
|
<div class="card mb-8">
|
||||||
<div style="display: grid; grid-template-columns: 1fr auto auto; gap: 1rem; align-items: end;">
|
<div style="display: grid; grid-template-columns: 1fr auto auto; gap: 1rem; align-items: end;">
|
||||||
<div>
|
<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 }}"
|
<input type="text" id="market-q" name="q" value="{{ current_q }}" placeholder="{{ t.mkt_search_placeholder }}"
|
||||||
class="form-input"
|
class="form-input"
|
||||||
hx-get="{{ url_for('content.market_results') }}"
|
hx-get="{{ url_for('content.market_results') }}"
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
hx-include="#market-country, #market-region">
|
hx-include="#market-country, #market-region">
|
||||||
</div>
|
</div>
|
||||||
<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"
|
<select id="market-country" name="country" class="form-input"
|
||||||
hx-get="{{ url_for('content.market_results') }}"
|
hx-get="{{ url_for('content.market_results') }}"
|
||||||
hx-target="#market-results"
|
hx-target="#market-results"
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<div class="scenario-widget scenario-capex">
|
<div class="scenario-widget scenario-capex">
|
||||||
<div class="scenario-widget__header">
|
<div class="scenario-widget__header">
|
||||||
<span class="scenario-widget__location">{{ scenario.location }}, {{ scenario.country }}</span>
|
<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>
|
||||||
<div class="scenario-widget__body">
|
<div class="scenario-widget__body">
|
||||||
<div class="scenario-widget__table-wrap">
|
<div class="scenario-widget__table-wrap">
|
||||||
<table class="scenario-widget__table">
|
<table class="scenario-widget__table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{% if lang == 'de' %}Position{% else %}Item{% endif %}</th>
|
<th>{{ t.scenario_table_item_label }}</th>
|
||||||
<th class="text-right">{% if lang == 'de' %}Betrag{% else %}Amount{% endif %}</th>
|
<th class="text-right">{{ t.scenario_table_amount_label }}</th>
|
||||||
<th>Detail</th>
|
<th>Detail</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
<tfoot>
|
<tfoot>
|
||||||
<tr>
|
<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 class="text-right mono"><strong>€{{ "{:,.0f}".format(d.capex) }}</strong></td>
|
||||||
<td></td>
|
<td></td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="scenario-widget__metrics" style="margin-top: 1rem;">
|
<div class="scenario-widget__metrics" style="margin-top: 1rem;">
|
||||||
<div class="scenario-widget__metric">
|
<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>
|
<span class="scenario-widget__metric-value">€{{ "{:,.0f}".format(d.capexPerCourt) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="scenario-widget__metric">
|
<div class="scenario-widget__metric">
|
||||||
@@ -47,20 +47,20 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="scenario-widget__metrics">
|
<div class="scenario-widget__metrics">
|
||||||
<div class="scenario-widget__metric">
|
<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>
|
<span class="scenario-widget__metric-value">€{{ "{:,.0f}".format(d.equity) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="scenario-widget__metric">
|
<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>
|
<span class="scenario-widget__metric-value">€{{ "{:,.0f}".format(d.loanAmount) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="scenario-widget__metric">
|
<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>
|
<span class="scenario-widget__metric-value">€{{ "{:,.0f}".format(d.capex) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="scenario-widget__cta">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<div class="scenario-widget scenario-cashflow">
|
<div class="scenario-widget scenario-cashflow">
|
||||||
<div class="scenario-widget__header">
|
<div class="scenario-widget__header">
|
||||||
<span class="scenario-widget__location">{{ scenario.location }}, {{ scenario.country }}</span>
|
<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>
|
||||||
<div class="scenario-widget__body">
|
<div class="scenario-widget__body">
|
||||||
<div class="scenario-widget__table-wrap">
|
<div class="scenario-widget__table-wrap">
|
||||||
@@ -10,13 +10,13 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th></th>
|
<th></th>
|
||||||
{% for a in d.annuals %}
|
{% 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 %}
|
{% endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{% if lang == 'de' %}Umsatz{% else %}Revenue{% endif %}</td>
|
<td>{{ t.scenario_revenue_label }}</td>
|
||||||
{% for a in d.annuals %}
|
{% for a in d.annuals %}
|
||||||
<td class="text-right mono">€{{ "{:,.0f}".format(a.revenue) }}</td>
|
<td class="text-right mono">€{{ "{:,.0f}".format(a.revenue) }}</td>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -28,19 +28,19 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{% if lang == 'de' %}Schuldendienst{% else %}Debt Service{% endif %}</td>
|
<td>{{ t.scenario_debt_service_label }}</td>
|
||||||
{% for a in d.annuals %}
|
{% for a in d.annuals %}
|
||||||
<td class="text-right mono">€{{ "{:,.0f}".format(a.ds) }}</td>
|
<td class="text-right mono">€{{ "{:,.0f}".format(a.ds) }}</td>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
<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 %}
|
{% for a in d.annuals %}
|
||||||
<td class="text-right mono"><strong>€{{ "{:,.0f}".format(a.ncf) }}</strong></td>
|
<td class="text-right mono"><strong>€{{ "{:,.0f}".format(a.ncf) }}</strong></td>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
<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) %}
|
{% set cum = namespace(total=-d.capex) %}
|
||||||
{% for a in d.annuals %}
|
{% for a in d.annuals %}
|
||||||
{% set cum.total = cum.total + a.ncf %}
|
{% set cum.total = cum.total + a.ncf %}
|
||||||
@@ -60,6 +60,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="scenario-widget__cta">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,32 +1,32 @@
|
|||||||
<div class="scenario-widget scenario-operating">
|
<div class="scenario-widget scenario-operating">
|
||||||
<div class="scenario-widget__header">
|
<div class="scenario-widget__header">
|
||||||
<span class="scenario-widget__location">{{ scenario.location }}, {{ scenario.country }}</span>
|
<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>
|
||||||
<div class="scenario-widget__body">
|
<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__metrics">
|
||||||
<div class="scenario-widget__metric">
|
<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>
|
<span class="scenario-widget__metric-value">€{{ "{:.0f}".format(d.weightedRate) }}/hr</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="scenario-widget__metric">
|
<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>
|
<span class="scenario-widget__metric-value">{{ s.utilTarget }}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="scenario-widget__metric">
|
<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>
|
<span class="scenario-widget__metric-value">{{ "{:,.0f}".format(d.bookedHoursMonth) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="scenario-widget__table-wrap">
|
||||||
<table class="scenario-widget__table">
|
<table class="scenario-widget__table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{% if lang == 'de' %}Position{% else %}Item{% endif %}</th>
|
<th>{{ t.scenario_table_item_label }}</th>
|
||||||
<th class="text-right">{% if lang == 'de' %}Monatlich{% else %}Monthly{% endif %}</th>
|
<th class="text-right">{{ t.scenario_table_monthly_label }}</th>
|
||||||
<th>Detail</th>
|
<th>Detail</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
<tfoot>
|
<tfoot>
|
||||||
<tr>
|
<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 class="text-right mono"><strong>€{{ "{:,.0f}".format(d.opex) }}</strong></td>
|
||||||
<td></td>
|
<td></td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -49,24 +49,24 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</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">
|
<div class="scenario-widget__table-wrap">
|
||||||
<table class="scenario-widget__table">
|
<table class="scenario-widget__table">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<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>
|
<td class="text-right mono">€{{ "{:,.0f}".format(d.grossRevMonth) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<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>
|
<td class="text-right mono">-€{{ "{:,.0f}".format(d.feeDeduction) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<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>
|
<td class="text-right mono">€{{ "{:,.0f}".format(d.netRevMonth) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<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>
|
<td class="text-right mono">-€{{ "{:,.0f}".format(d.opex) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -74,11 +74,11 @@
|
|||||||
<td class="text-right mono"><strong>€{{ "{:,.0f}".format(d.ebitdaMonth) }}</strong></td>
|
<td class="text-right mono"><strong>€{{ "{:,.0f}".format(d.ebitdaMonth) }}</strong></td>
|
||||||
</tr>
|
</tr>
|
||||||
<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>
|
<td class="text-right mono">-€{{ "{:,.0f}".format(d.monthlyPayment) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<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>
|
<td class="text-right mono"><strong>€{{ "{:,.0f}".format(d.netCFMonth) }}</strong></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -86,6 +86,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="scenario-widget__cta">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<div class="scenario-widget scenario-returns">
|
<div class="scenario-widget scenario-returns">
|
||||||
<div class="scenario-widget__header">
|
<div class="scenario-widget__header">
|
||||||
<span class="scenario-widget__location">{{ scenario.location }}, {{ scenario.country }}</span>
|
<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>
|
||||||
<div class="scenario-widget__body">
|
<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__metrics">
|
||||||
<div class="scenario-widget__metric">
|
<div class="scenario-widget__metric">
|
||||||
<span class="scenario-widget__metric-label">IRR ({{ s.holdYears }}yr)</span>
|
<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>
|
<span class="scenario-widget__metric-value">{{ "{:.2f}".format(d.moic) }}x</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="scenario-widget__metric">
|
<div class="scenario-widget__metric">
|
||||||
<span class="scenario-widget__metric-label">{% if lang == 'de' %}Amortisation{% else %}Payback{% 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 }} {% if lang == 'de' %}Monate{% else %}months{% endif %}{% else %}N/A{% endif %}</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>
|
</div>
|
||||||
<div class="scenario-widget__metrics">
|
<div class="scenario-widget__metrics">
|
||||||
@@ -25,48 +25,48 @@
|
|||||||
<span class="scenario-widget__metric-value">{{ "{:.1f}".format(d.cashOnCash * 100) }}%</span>
|
<span class="scenario-widget__metric-value">{{ "{:.1f}".format(d.cashOnCash * 100) }}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="scenario-widget__metric">
|
<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>
|
<span class="scenario-widget__metric-value">{{ "{:.1f}".format(d.yieldOnCost * 100) }}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="scenario-widget__metric">
|
<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>
|
<span class="scenario-widget__metric-value">{{ "{:.0f}".format(d.ebitdaMargin * 100) }}%</span>
|
||||||
</div>
|
</div>
|
||||||
</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__metrics">
|
||||||
<div class="scenario-widget__metric">
|
<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>
|
<span class="scenario-widget__metric-value">€{{ "{:,.0f}".format(d.exitValue) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="scenario-widget__metric">
|
<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>
|
<span class="scenario-widget__metric-value">€{{ "{:,.0f}".format(d.remainingLoan) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="scenario-widget__metric">
|
<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>
|
<span class="scenario-widget__metric-value">€{{ "{:,.0f}".format(d.netExit) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</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__metrics">
|
||||||
<div class="scenario-widget__metric">
|
<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>
|
<span class="scenario-widget__metric-value">€{{ "{:,.0f}".format(d.loanAmount) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="scenario-widget__metric">
|
<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>
|
<span class="scenario-widget__metric-value">{{ s.interestRate }}% / {{ s.loanTerm }}yr</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="scenario-widget__metric">
|
<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>
|
<span class="scenario-widget__metric-value">€{{ "{:,.0f}".format(d.monthlyPayment) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="scenario-widget__cta">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,11 +6,11 @@
|
|||||||
<div class="scenario-widget__body">
|
<div class="scenario-widget__body">
|
||||||
<div class="scenario-widget__metrics">
|
<div class="scenario-widget__metrics">
|
||||||
<div class="scenario-widget__metric">
|
<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>
|
<span class="scenario-widget__metric-value">€{{ "{:,.0f}".format(d.capex) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="scenario-widget__metric">
|
<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>
|
<span class="scenario-widget__metric-value">€{{ "{:,.0f}".format(d.ebitdaMonth) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="scenario-widget__metric">
|
<div class="scenario-widget__metric">
|
||||||
@@ -20,20 +20,20 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="scenario-widget__metrics">
|
<div class="scenario-widget__metrics">
|
||||||
<div class="scenario-widget__metric">
|
<div class="scenario-widget__metric">
|
||||||
<span class="scenario-widget__metric-label">{% if lang == 'de' %}Amortisation{% else %}Payback{% 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 }} {% if lang == 'de' %}Monate{% else %}months{% endif %}{% else %}N/A{% endif %}</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__metric">
|
<div class="scenario-widget__metric">
|
||||||
<span class="scenario-widget__metric-label">Cash-on-Cash</span>
|
<span class="scenario-widget__metric-label">Cash-on-Cash</span>
|
||||||
<span class="scenario-widget__metric-value">{{ "{:.1f}".format(d.cashOnCash * 100) }}%</span>
|
<span class="scenario-widget__metric-value">{{ "{:.1f}".format(d.cashOnCash * 100) }}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="scenario-widget__metric">
|
<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>
|
<span class="scenario-widget__metric-value">{{ "{:.0f}".format(d.ebitdaMargin * 100) }}%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="scenario-widget__cta">
|
<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>
|
||||||
</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 ..auth.routes import login_required, update_user
|
||||||
from ..core import csrf_protect, fetch_one, soft_delete
|
from ..core import csrf_protect, fetch_one, soft_delete
|
||||||
|
from ..i18n import get_translations
|
||||||
|
|
||||||
bp = Blueprint(
|
bp = Blueprint(
|
||||||
"dashboard",
|
"dashboard",
|
||||||
@@ -58,7 +59,8 @@ async def settings():
|
|||||||
name=form.get("name", "").strip() or None,
|
name=form.get("name", "").strip() or None,
|
||||||
updated_at=datetime.utcnow().isoformat(),
|
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 redirect(url_for("dashboard.settings"))
|
||||||
|
|
||||||
return await render_template("settings.html")
|
return await render_template("settings.html")
|
||||||
@@ -71,5 +73,6 @@ async def delete_account():
|
|||||||
from quart import session
|
from quart import session
|
||||||
await soft_delete("users", g.user["id"])
|
await soft_delete("users", g.user["id"])
|
||||||
session.clear()
|
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"))
|
return redirect(url_for("public.landing"))
|
||||||
|
|||||||
@@ -1,34 +1,34 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}Dashboard - {{ config.APP_NAME }}{% endblock %}
|
{% block title %}{{ t.dash_page_title }} - {{ config.APP_NAME }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main class="container-page py-12">
|
<main class="container-page py-12">
|
||||||
<h1 class="text-2xl mb-1">Dashboard</h1>
|
<h1 class="text-2xl mb-1">{{ t.dash_h1 }}</h1>
|
||||||
<p class="text-slate mb-8">Welcome back{% if user.name %}, {{ user.name }}{% endif %}!</p>
|
<p class="text-slate mb-8">{{ t.dash_welcome }}{% if user.name %}, {{ user.name }}{% endif %}!</p>
|
||||||
|
|
||||||
<div class="grid-3 mb-10">
|
<div class="grid-3 mb-10">
|
||||||
<div class="card text-center">
|
<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-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>
|
||||||
<div class="card text-center">
|
<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-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>
|
||||||
<div class="card text-center">
|
<div class="card text-center">
|
||||||
<p class="card-header">Plan</p>
|
<p class="card-header">{{ t.dash_plan }}</p>
|
||||||
<p class="text-3xl font-bold text-navy">Free</p>
|
<p class="text-3xl font-bold text-navy">{{ t.dash_plan_free }}</p>
|
||||||
<p class="text-xs text-slate mt-1">Full access to all features</p>
|
<p class="text-xs text-slate mt-1">{{ t.dash_plan_free_sub }}</p>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="grid-3">
|
||||||
<a href="{{ url_for('planner.index') }}" class="btn text-center">Open Planner</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">Get Supplier Quotes</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">Settings</a>
|
<a href="{{ url_for('dashboard.settings') }}" class="btn-outline text-center">{{ t.dash_settings }}</a>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,43 +1,43 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}Settings - {{ config.APP_NAME }}{% endblock %}
|
{% block title %}{{ t.dash_settings_title }} - {{ config.APP_NAME }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main class="container-page py-12">
|
<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">
|
<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">
|
<div class="card">
|
||||||
<form method="post" class="space-y-4">
|
<form method="post" class="space-y-4">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
|
||||||
<div>
|
<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>
|
<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>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="name" class="form-label">Name</label>
|
<label for="name" class="form-label">{{ t.dash_name_label }}</label>
|
||||||
<input type="text" id="name" name="name" value="{{ user.name or '' }}" placeholder="Your name" class="form-input">
|
<input type="text" id="name" name="name" value="{{ user.name or '' }}" placeholder="{{ t.dash_name_placeholder }}" class="form-input">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn">Save Changes</button>
|
<button type="submit" class="btn">{{ t.dash_save_changes }}</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<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">
|
<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>
|
<details>
|
||||||
<summary class="cursor-pointer text-sm font-semibold text-danger">Delete Account</summary>
|
<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">This will delete all your scenarios and data permanently.</p>
|
<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') }}">
|
<form method="post" action="{{ url_for('dashboard.delete_account') }}">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<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>
|
</form>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
{% extends "base.html" %}
|
{% 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 %}
|
{% block head %}
|
||||||
{% if lang == 'de' %}
|
<meta name="description" content="{{ t.dir_page_meta_desc | tformat(count=total_suppliers, countries=total_countries) }}">
|
||||||
<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="{{ t.dir_page_title }} - {{ config.APP_NAME }}">
|
||||||
<meta property="og:title" content="Padel-Platz Anbieterverzeichnis - {{ config.APP_NAME }}">
|
<meta property="og:description" content="{{ t.dir_page_og_desc | tformat(count=total_suppliers, countries=total_countries) }}">
|
||||||
<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 %}
|
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--dir-green: #15803D;
|
--dir-green: #15803D;
|
||||||
@@ -305,7 +299,7 @@
|
|||||||
<main class="container-page">
|
<main class="container-page">
|
||||||
<div class="dir-hero">
|
<div class="dir-hero">
|
||||||
<h1>{{ t.dir_heading }}</h1>
|
<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">
|
<div class="dir-stats">
|
||||||
<span><strong>{{ total_suppliers }}</strong> {{ t.dir_stat_suppliers }}</span>
|
<span><strong>{{ total_suppliers }}</strong> {{ t.dir_stat_suppliers }}</span>
|
||||||
<span><strong>{{ total_countries }}</strong> {{ t.dir_stat_countries }}</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-weight:700;color:#16A34A;margin-bottom:4px">{{ t.enquiry_success_title }}</p>
|
||||||
<p style="font-size:0.8125rem;color:#166534">
|
<p style="font-size:0.8125rem;color:#166534">
|
||||||
{% if supplier and supplier.contact_email %}
|
{% 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 %}
|
{% 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 %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<p style="font-size:0.8125rem;color:#64748B;margin-bottom:1rem">
|
<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 %}
|
{% if page > 1 %} (page {{ page }}){% endif %}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -38,19 +38,19 @@
|
|||||||
<div class="ph-grid"></div>
|
<div class="ph-grid"></div>
|
||||||
<div class="example-icon">
|
<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>
|
<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>
|
||||||
<div class="dir-card__featured" style="background:#3B82F6">{% if lang == 'de' %}Beispiel{% else %}Example{% endif %}</div>
|
<div class="dir-card__featured" style="background:#3B82F6">{{ t.dir_ex_badge }}</div>
|
||||||
<div class="dir-card__cat dir-card__cat--example">{% if lang == 'de' %}Deine Kategorie{% else %}Your Category{% endif %}</div>
|
<div class="dir-card__cat dir-card__cat--example">{{ t.dir_ex_category }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dir-card__body">
|
<div class="dir-card__body">
|
||||||
<div class="dir-card__logo-wrap">
|
<div class="dir-card__logo-wrap">
|
||||||
<div class="dir-card__logo-ph">?</div>
|
<div class="dir-card__logo-ph">?</div>
|
||||||
</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">
|
<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>
|
<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>
|
</p>
|
||||||
<div class="dir-card__stats">
|
<div class="dir-card__stats">
|
||||||
<span class="dir-card__stat dir-card__stat--verified">
|
<span class="dir-card__stat dir-card__stat--verified">
|
||||||
@@ -59,10 +59,10 @@
|
|||||||
</span>
|
</span>
|
||||||
<span class="dir-card__stat">12 projects · 8 yrs</span>
|
<span class="dir-card__stat">12 projects · 8 yrs</span>
|
||||||
</div>
|
</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">
|
<div class="dir-card__foot">
|
||||||
<span></span>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{% extends "base.html" %}
|
{% 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 %}
|
{% block head %}
|
||||||
{% set _sup_country = country_labels.get(supplier.country_code, supplier.country_code) %}
|
{% set _sup_country = country_labels.get(supplier.country_code, supplier.country_code) %}
|
||||||
@@ -361,7 +361,7 @@
|
|||||||
<div class="sp-enquiry-field">
|
<div class="sp-enquiry-field">
|
||||||
<label class="sp-enquiry-label">{{ t.sp_enquiry_message }} <span style="color:#EF4444">*</span></label>
|
<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
|
<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>
|
</div>
|
||||||
<button type="submit" class="sp-enquiry-submit">{{ t.sp_enquiry_submit }}</button>
|
<button type="submit" class="sp-enquiry-submit">{{ t.sp_enquiry_submit }}</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -461,10 +461,10 @@
|
|||||||
<div class="sp-cta-strip">
|
<div class="sp-cta-strip">
|
||||||
<div class="sp-cta-strip__text">
|
<div class="sp-cta-strip__text">
|
||||||
<h3>{{ t.sp_cta_basic_h3 }}</h3>
|
<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>
|
</div>
|
||||||
<a href="{{ url_for('suppliers.signup') }}" class="sp-cta-strip__btn">
|
<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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% elif supplier.tier == 'growth' %}
|
{% elif supplier.tier == 'growth' %}
|
||||||
@@ -500,7 +500,7 @@
|
|||||||
<div class="sp-locked-popover" id="locked-popover" role="tooltip">
|
<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__title">{{ t.sp_locked_popover_title }}</p>
|
||||||
<p class="sp-locked-popover__body">
|
<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>
|
</p>
|
||||||
<a href="{{ url_for('leads.quote_request', country=supplier.country_code) }}"
|
<a href="{{ url_for('leads.quote_request', country=supplier.country_code) }}"
|
||||||
class="sp-locked-popover__link">{{ t.sp_locked_popover_link }}</a>
|
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">
|
||||||
<div class="sp-cta-strip__text">
|
<div class="sp-cta-strip__text">
|
||||||
<h3>{{ t.sp_cta_claim_h3 }}</h3>
|
<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>
|
</div>
|
||||||
<a href="{{ url_for('suppliers.claim', slug=supplier.slug) }}" class="sp-cta-strip__btn sp-cta-strip__btn--green">
|
<a href="{{ url_for('suppliers.claim', slug=supplier.slug) }}" class="sp-cta-strip__btn sp-cta-strip__btn--green">
|
||||||
{{ t.sp_cta_claim_btn }}
|
{{ t.sp_cta_claim_btn }}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
<div class="q-field-group">
|
<div class="q-field-group">
|
||||||
<span class="q-label">{{ t.q1_facility_label }} <span class="required">*</span></span>
|
<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">
|
<div class="q-pills">
|
||||||
{% for val, label in [('indoor', t.q1_facility_indoor), ('outdoor', t.q1_facility_outdoor), ('both', t.q1_facility_both)] %}
|
{% 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>
|
<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 id="q-progress" hx-swap-oob="innerHTML">
|
||||||
<div class="q-progress__meta">
|
<div class="q-progress__meta">
|
||||||
<span class="q-progress__label">{{ steps[step - 1].title }}</span>
|
<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>
|
||||||
<div class="q-progress__track">
|
<div class="q-progress__track">
|
||||||
<div class="q-progress__fill" style="width: {{ (step / steps|length * 100) | round }}%"></div>
|
<div class="q-progress__fill" style="width: {{ (step / steps|length * 100) | round }}%"></div>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
<div class="q-field-group">
|
<div class="q-field-group">
|
||||||
<label class="q-label" for="country">{{ t.q2_country_label }} <span class="required">*</span></label>
|
<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 %}">
|
<select id="country" name="country" class="q-input {% if 'country' in errors %}q-input--error{% endif %}">
|
||||||
<option value="">{{ t.q2_country_default }}</option>
|
<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')] %}
|
{% 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 id="q-progress" hx-swap-oob="innerHTML">
|
||||||
<div class="q-progress__meta">
|
<div class="q-progress__meta">
|
||||||
<span class="q-progress__label">{{ steps[step - 1].title }}</span>
|
<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>
|
||||||
<div class="q-progress__track">
|
<div class="q-progress__track">
|
||||||
<div class="q-progress__fill" style="width: {{ (step / steps|length * 100) | round }}%"></div>
|
<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 id="q-progress" hx-swap-oob="innerHTML">
|
||||||
<div class="q-progress__meta">
|
<div class="q-progress__meta">
|
||||||
<span class="q-progress__label">{{ steps[step - 1].title }}</span>
|
<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>
|
||||||
<div class="q-progress__track">
|
<div class="q-progress__track">
|
||||||
<div class="q-progress__fill" style="width: {{ (step / steps|length * 100) | round }}%"></div>
|
<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 id="q-progress" hx-swap-oob="innerHTML">
|
||||||
<div class="q-progress__meta">
|
<div class="q-progress__meta">
|
||||||
<span class="q-progress__label">{{ steps[step - 1].title }}</span>
|
<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>
|
||||||
<div class="q-progress__track">
|
<div class="q-progress__track">
|
||||||
<div class="q-progress__fill" style="width: {{ (step / steps|length * 100) | round }}%"></div>
|
<div class="q-progress__fill" style="width: {{ (step / steps|length * 100) | round }}%"></div>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
<div class="q-field-group">
|
<div class="q-field-group">
|
||||||
<span class="q-label">{{ t.q5_timeline_label }} <span class="required">*</span></span>
|
<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">
|
<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)] %}
|
{% 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>
|
<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 id="q-progress" hx-swap-oob="innerHTML">
|
||||||
<div class="q-progress__meta">
|
<div class="q-progress__meta">
|
||||||
<span class="q-progress__label">{{ steps[step - 1].title }}</span>
|
<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>
|
||||||
<div class="q-progress__track">
|
<div class="q-progress__track">
|
||||||
<div class="q-progress__fill" style="width: {{ (step / steps|length * 100) | round }}%"></div>
|
<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 id="q-progress" hx-swap-oob="innerHTML">
|
||||||
<div class="q-progress__meta">
|
<div class="q-progress__meta">
|
||||||
<span class="q-progress__label">{{ steps[step - 1].title }}</span>
|
<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>
|
||||||
<div class="q-progress__track">
|
<div class="q-progress__track">
|
||||||
<div class="q-progress__fill" style="width: {{ (step / steps|length * 100) | round }}%"></div>
|
<div class="q-progress__fill" style="width: {{ (step / steps|length * 100) | round }}%"></div>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
<div class="q-field-group">
|
<div class="q-field-group">
|
||||||
<span class="q-label">{{ t.q7_role_label }} <span class="required">*</span></span>
|
<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">
|
<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)] %}
|
{% 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>
|
<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 id="q-progress" hx-swap-oob="innerHTML">
|
||||||
<div class="q-progress__meta">
|
<div class="q-progress__meta">
|
||||||
<span class="q-progress__label">{{ steps[step - 1].title }}</span>
|
<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>
|
||||||
<div class="q-progress__track">
|
<div class="q-progress__track">
|
||||||
<div class="q-progress__fill" style="width: {{ (step / steps|length * 100) | round }}%"></div>
|
<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 id="q-progress" hx-swap-oob="innerHTML">
|
||||||
<div class="q-progress__meta">
|
<div class="q-progress__meta">
|
||||||
<span class="q-progress__label">{{ steps[step - 1].title }}</span>
|
<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>
|
||||||
<div class="q-progress__track">
|
<div class="q-progress__track">
|
||||||
<div class="q-progress__fill" style="width: {{ (step / steps|length * 100) | round }}%"></div>
|
<div class="q-progress__fill" style="width: {{ (step / steps|length * 100) | round }}%"></div>
|
||||||
|
|||||||
@@ -21,19 +21,19 @@
|
|||||||
|
|
||||||
<div class="q-field-group">
|
<div class="q-field-group">
|
||||||
<label class="q-label" for="contact_name">{{ t.q9_name_label }} <span class="required">*</span></label>
|
<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>
|
<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>
|
||||||
|
|
||||||
<div class="q-field-group">
|
<div class="q-field-group">
|
||||||
<label class="q-label" for="contact_email">{{ t.q9_email_label }} <span class="required">*</span></label>
|
<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>
|
<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>
|
||||||
|
|
||||||
<div class="q-field-group">
|
<div class="q-field-group">
|
||||||
<label class="q-label" for="contact_phone">{{ t.q9_phone_label }} <span class="required">*</span></label>
|
<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>
|
<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>
|
</div>
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
<div id="q-progress" hx-swap-oob="innerHTML">
|
<div id="q-progress" hx-swap-oob="innerHTML">
|
||||||
<div class="q-progress__meta">
|
<div class="q-progress__meta">
|
||||||
<span class="q-progress__label">{{ steps[step - 1].title }}</span>
|
<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>
|
||||||
<div class="q-progress__track">
|
<div class="q-progress__track">
|
||||||
<div class="q-progress__fill" style="width: {{ (step / steps|length * 100) | round }}%"></div>
|
<div class="q-progress__fill" style="width: {{ (step / steps|length * 100) | round }}%"></div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{% extends "base.html" %}
|
{% 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 %}
|
{% block head %}
|
||||||
<style>
|
<style>
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
<div class="q-progress" id="q-progress">
|
<div class="q-progress" id="q-progress">
|
||||||
<div class="q-progress__meta">
|
<div class="q-progress__meta">
|
||||||
<span class="q-progress__label">{{ steps[step - 1].title }}</span>
|
<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>
|
||||||
<div class="q-progress__track">
|
<div class="q-progress__track">
|
||||||
<div class="q-progress__fill" style="width: {{ (step / steps|length * 100) | round }}%"></div>
|
<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>
|
<h1 class="text-2xl" style="margin-bottom: 0.5rem;">{{ t.qs_title }}</h1>
|
||||||
<p style="color: #64748B; font-size: 0.9375rem;">
|
<p style="color: #64748B; font-size: 0.9375rem;">
|
||||||
{% if lang == 'de' %}
|
{{ t.qs_matched_pre }}
|
||||||
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.
|
{% if court_count %}{{ court_count }}{{ t.qs_matched_court_suffix }}{% endif %}
|
||||||
{% else %}
|
{% if facility_type %}{{ t.qs_matched_facility_fmt | tformat(type=facility_type) }}{% endif %}
|
||||||
We've matched your
|
{{ t.qs_matched_project }}
|
||||||
{% if court_count %}{{ court_count }}-court{% endif %}
|
|
||||||
{% if facility_type %}{{ facility_type }}{% endif %}
|
|
||||||
project
|
|
||||||
{% if country %}in {{ country }}{% endif %}
|
{% if country %}in {{ country }}{% endif %}
|
||||||
with verified suppliers who'll reach out with tailored proposals.
|
{{ t.qs_matched_post }}
|
||||||
{% endif %}
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="next-steps">
|
<div class="next-steps">
|
||||||
|
|||||||
@@ -8,11 +8,11 @@
|
|||||||
|
|
||||||
<h1 class="text-2xl mb-4">{{ t.qv_heading }}</h1>
|
<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="font-semibold text-navy my-2">{{ contact_email }}</p>
|
||||||
|
|
||||||
<p class="text-slate text-sm" style="margin-top: 1rem;">
|
<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>
|
||||||
|
|
||||||
<p class="text-slate text-sm" style="margin-top: 0.5rem;">
|
<p class="text-slate text-sm" style="margin-top: 0.5rem;">
|
||||||
@@ -22,10 +22,10 @@
|
|||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<details class="text-left">
|
<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">
|
<ul class="list-disc pl-6 mt-2 space-y-1 text-sm text-slate-dark">
|
||||||
<li>{{ t.qv_spam }}</li>
|
<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>
|
<li>{{ t.qv_wait }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p class="text-sm text-slate mt-3">
|
<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>
|
<div class="exp-price">€99 <span>one-time</span></div>
|
||||||
|
|
||||||
<ul class="exp-features">
|
<ul class="exp-features">
|
||||||
{% if lang == 'de' %}
|
{% 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>Zusammenfassung</li>
|
<li>{{ t[key] }}</li>
|
||||||
<li>CAPEX-Aufschlüsselung</li>
|
{% endfor %}
|
||||||
<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 %}
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="exp-form">
|
<div class="exp-form">
|
||||||
|
|||||||
@@ -7,89 +7,30 @@
|
|||||||
<div class="card max-w-md mx-auto mt-8 text-center">
|
<div class="card max-w-md mx-auto mt-8 text-center">
|
||||||
<h1 class="text-2xl mb-4">{{ t.export_waitlist_title }}</h1>
|
<h1 class="text-2xl mb-4">{{ t.export_waitlist_title }}</h1>
|
||||||
|
|
||||||
{% if lang == 'de' %}
|
<p class="text-slate-dark mb-6">{{ t.planner_ewl_intro }}</p>
|
||||||
<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>
|
|
||||||
|
|
||||||
<div class="bg-slate-50 border border-slate-200 rounded-lg p-4 mb-6 text-left">
|
<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">
|
<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">
|
<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">
|
<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>
|
<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>
|
</svg>
|
||||||
<span>Professioneller Businessplan (20+ Seiten als PDF)</span>
|
<span>{{ t[key] }}</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>
|
|
||||||
</li>
|
</li>
|
||||||
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-sm text-slate mb-6">
|
<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">
|
<ul class="mt-2 text-slate-dark">
|
||||||
<li>• Frühem Zugang mit Sonderpreis</li>
|
<li>• {{ t.planner_ewl_email_item1 }}</li>
|
||||||
<li>• Launch-Rabatt</li>
|
<li>• {{ t.planner_ewl_email_item2 }}</li>
|
||||||
<li>• Vorrangiger Generierungswarteschlange</li>
|
<li>• {{ t.planner_ewl_email_item3 }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</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>
|
<a href="{{ url_for('planner.index') }}" class="btn w-full">{{ t.export_waitlist_btn }}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div class="chart-container mt-4">
|
<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">
|
<div class="chart-h-56 chart-container__canvas">
|
||||||
<canvas id="chartCapex"></canvas>
|
<canvas id="chartCapex"></canvas>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -25,19 +25,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="chart-container mb-4">
|
<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 class="chart-h-56 chart-container__canvas"><canvas id="chartCF"></canvas></div>
|
||||||
</div>
|
</div>
|
||||||
<script type="application/json" id="chartCF-data">{{ d.cf_chart | tojson }}</script>
|
<script type="application/json" id="chartCF-data">{{ d.cf_chart | tojson }}</script>
|
||||||
|
|
||||||
<div class="chart-container mb-4">
|
<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 class="chart-h-48 chart-container__canvas"><canvas id="chartCum"></canvas></div>
|
||||||
</div>
|
</div>
|
||||||
<script type="application/json" id="chartCum-data">{{ d.cum_chart | tojson }}</script>
|
<script type="application/json" id="chartCum-data">{{ d.cum_chart | tojson }}</script>
|
||||||
|
|
||||||
<div class="mb-section">
|
<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">
|
<table class="data-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -25,11 +25,11 @@
|
|||||||
|
|
||||||
<div class="grid-2 mb-4">
|
<div class="grid-2 mb-4">
|
||||||
<div class="chart-container">
|
<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 class="chart-h-48 chart-container__canvas"><canvas id="chartRevRamp"></canvas></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart-container">
|
<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 class="chart-h-48 chart-container__canvas"><canvas id="chartPL"></canvas></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
<script type="application/json" id="chartPL-data">{{ d.pl_chart | tojson }}</script>
|
<script type="application/json" id="chartPL-data">{{ d.pl_chart | tojson }}</script>
|
||||||
|
|
||||||
<div class="mb-section">
|
<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 = [
|
{% set streams = [
|
||||||
(t.stream_court_rental, d.courtRevMonth - d.feeDeduction),
|
(t.stream_court_rental, d.courtRevMonth - d.feeDeduction),
|
||||||
(t.stream_equipment, d.racketRev + d.ballMargin),
|
(t.stream_equipment, d.racketRev + d.ballMargin),
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-section">
|
<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">
|
<table class="data-table">
|
||||||
<thead><tr><th>{{ t.th_item }}</th><th class="right">{{ t.th_monthly }}</th></tr></thead>
|
<thead><tr><th>{{ t.th_item }}</th><th class="right">{{ t.th_monthly }}</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -88,7 +88,7 @@
|
|||||||
|
|
||||||
{% if s.venue == 'outdoor' %}
|
{% if s.venue == 'outdoor' %}
|
||||||
<div class="mb-section season-section visible">
|
<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 class="chart-container"><div class="chart-h-40 chart-container__canvas"><canvas id="chartSeason"></canvas></div></div>
|
||||||
</div>
|
</div>
|
||||||
<script type="application/json" id="chartSeason-data">{{ d.season_chart | tojson }}</script>
|
<script type="application/json" id="chartSeason-data">{{ d.season_chart | tojson }}</script>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
<div class="grid-2 mb-4">
|
<div class="grid-2 mb-4">
|
||||||
<div class="chart-container">
|
<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">
|
<div id="exitWaterfall" style="margin-top:10px">
|
||||||
{% set wf_rows = [
|
{% set wf_rows = [
|
||||||
(t.wf_stab_ebitda, d.stabEbitda | int | fmt_currency, 'c-head'),
|
(t.wf_stab_ebitda, d.stabEbitda | int | fmt_currency, 'c-head'),
|
||||||
@@ -45,14 +45,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart-container">
|
<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 class="chart-h-44 chart-container__canvas"><canvas id="chartDSCR"></canvas></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script type="application/json" id="chartDSCR-data">{{ d.dscr_chart | tojson }}</script>
|
<script type="application/json" id="chartDSCR-data">{{ d.dscr_chart | tojson }}</script>
|
||||||
|
|
||||||
<div class="mb-section">
|
<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">
|
<table class="data-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-section">
|
<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">
|
<table class="data-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -1,16 +1,10 @@
|
|||||||
{% extends "base.html" %}
|
{% 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 %}
|
{% block head %}
|
||||||
{% if lang == 'de' %}
|
<meta name="description" content="{{ t.planner_meta_desc }}">
|
||||||
<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="{{ t.planner_page_title }} - {{ config.APP_NAME }}">
|
||||||
<meta property="og:title" content="Padel-Platz Finanzrechner - {{ config.APP_NAME }}">
|
<meta property="og:description" content="{{ t.planner_meta_desc }}">
|
||||||
<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 property="og:type" content="website">
|
<meta property="og:type" content="website">
|
||||||
<meta property="og:image" content="{{ url_for('static', filename='images/planner-screenshot.png', _external=True) }}">
|
<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') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/planner.css') }}">
|
||||||
@@ -45,7 +39,7 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="planner-app">
|
<div class="planner-app">
|
||||||
<header class="planner-header">
|
<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>
|
<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 %}
|
{% if user %}
|
||||||
@@ -109,16 +103,11 @@
|
|||||||
|
|
||||||
<!-- Step 1: Venue -->
|
<!-- Step 1: Venue -->
|
||||||
<div class="wizard-step active" data-wiz="1">
|
<div class="wizard-step active" data-wiz="1">
|
||||||
{% if lang == 'de' %}
|
<h2 class="wizard-step__title">{{ t.planner_step1_title }}</h2>
|
||||||
<h2 class="wizard-step__title">Dein Padel-Platz</h2>
|
<p class="wizard-step__sub">{{ t.planner_step1_sub }}</p>
|
||||||
<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 %}
|
|
||||||
|
|
||||||
<div class="mb-section">
|
<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">
|
<div class="toggle-group">
|
||||||
<button type="button" class="toggle-btn {{ 'toggle-btn--active' if s.venue == 'indoor' }}"
|
<button type="button" class="toggle-btn {{ 'toggle-btn--active' if s.venue == 'indoor' }}"
|
||||||
data-toggle="venue" data-val="indoor"
|
data-toggle="venue" data-val="indoor"
|
||||||
@@ -131,7 +120,7 @@
|
|||||||
hx-target="#tab-content" hx-include="#planner-form"
|
hx-target="#tab-content" hx-include="#planner-form"
|
||||||
onclick="setToggle(this,'venue','outdoor')">{{ t.toggle_outdoor }}</button>
|
onclick="setToggle(this,'venue','outdoor')">{{ t.toggle_outdoor }}</button>
|
||||||
</div>
|
</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">
|
<div class="toggle-group">
|
||||||
<button type="button" class="toggle-btn {{ 'toggle-btn--active' if s.own == 'rent' }}"
|
<button type="button" class="toggle-btn {{ 'toggle-btn--active' if s.own == 'rent' }}"
|
||||||
data-toggle="own" data-val="rent"
|
data-toggle="own" data-val="rent"
|
||||||
@@ -163,19 +152,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-section">
|
<div class="mb-section">
|
||||||
{% if lang == 'de' %}
|
<div class="section-header"><h3>{{ t.planner_section_court_config }}</h3></div>
|
||||||
<div class="section-header"><h3>Platzkonfiguration</h3></div>
|
|
||||||
{% else %}
|
|
||||||
<div class="section-header"><h3>Court Configuration</h3></div>
|
|
||||||
{% endif %}
|
|
||||||
{{ slider('dblCourts', t.sl_dbl_courts, 0, 30, 1, s.dblCourts, t.tip_dbl_courts) }}
|
{{ 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) }}
|
{{ 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>{{ t.planner_section_space_req }}</h3></div>
|
||||||
<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 data-show-venue="indoor">
|
<div data-show-venue="indoor">
|
||||||
{{ slider('sqmPerDblHall', t.sl_sqm_dbl_hall, 200, 600, 10, s.sqmPerDblHall, t.tip_sqm_dbl_hall) }}
|
{{ 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) }}
|
{{ 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 -->
|
<!-- Step 2: Pricing & Utilization -->
|
||||||
<div class="wizard-step" data-wiz="2">
|
<div class="wizard-step" data-wiz="2">
|
||||||
{% if lang == 'de' %}
|
<h2 class="wizard-step__title">{{ t.planner_step2_title }}</h2>
|
||||||
<h2 class="wizard-step__title">Preise & Auslastung</h2>
|
<p class="wizard-step__sub">{{ t.planner_step2_sub }}</p>
|
||||||
<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 %}
|
|
||||||
|
|
||||||
<div class="mb-section">
|
<div class="mb-section">
|
||||||
{% if lang == 'de' %}
|
<div class="section-header"><h3>{{ t.planner_section_pricing }}</h3><span class="hint">{{ t.planner_hint_per_court }}</span></div>
|
||||||
<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 %}
|
|
||||||
{{ slider('ratePeak', t.sl_rate_peak, 0, 150, 1, s.ratePeak, t.tip_rate_peak) }}
|
{{ 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('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) }}
|
{{ slider('rateSingle', t.sl_rate_single, 0, 150, 1, s.rateSingle, t.tip_rate_single) }}
|
||||||
@@ -212,11 +184,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-section">
|
<div class="mb-section">
|
||||||
{% if lang == 'de' %}
|
<div class="section-header"><h3>{{ t.planner_section_util }}</h3></div>
|
||||||
<div class="section-header"><h3>Auslastung & Betrieb</h3></div>
|
|
||||||
{% else %}
|
|
||||||
<div class="section-header"><h3>Utilization & Operations</h3></div>
|
|
||||||
{% endif %}
|
|
||||||
{{ slider('utilTarget', t.sl_util_target, 0, 100, 1, s.utilTarget, t.tip_util_target) }}
|
{{ 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('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) }}
|
{{ slider('daysPerMonthIndoor', t.sl_days_indoor, 0, 31, 1, s.daysPerMonthIndoor, t.tip_days_indoor) }}
|
||||||
@@ -231,20 +199,11 @@
|
|||||||
|
|
||||||
<!-- Step 3: Investment & Build Costs -->
|
<!-- Step 3: Investment & Build Costs -->
|
||||||
<div class="wizard-step" data-wiz="3">
|
<div class="wizard-step" data-wiz="3">
|
||||||
{% if lang == 'de' %}
|
<h2 class="wizard-step__title">{{ t.planner_step3_title }}</h2>
|
||||||
<h2 class="wizard-step__title">Investition & Baukosten</h2>
|
<p class="wizard-step__sub">{{ t.planner_step3_sub }}</p>
|
||||||
<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 %}
|
|
||||||
|
|
||||||
<div class="mb-section">
|
<div class="mb-section">
|
||||||
{% if lang == 'de' %}
|
<div class="section-header"><h3>{{ t.planner_section_capex }}</h3><span class="hint">{{ t.planner_hint_adjust }}</span></div>
|
||||||
<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="pill-group">
|
<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>
|
<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 -->
|
<!-- Step 4: Operations & Financing -->
|
||||||
<div class="wizard-step" data-wiz="4">
|
<div class="wizard-step" data-wiz="4">
|
||||||
{% if lang == 'de' %}
|
<h2 class="wizard-step__title">{{ t.planner_step4_title }}</h2>
|
||||||
<h2 class="wizard-step__title">Betrieb & Finanzierung</h2>
|
<p class="wizard-step__sub">{{ t.planner_step4_sub }}</p>
|
||||||
<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 %}
|
|
||||||
|
|
||||||
<div class="mb-section">
|
<div class="mb-section">
|
||||||
{% if lang == 'de' %}<div class="section-header"><h3>Monatliche Betriebskosten</h3></div>
|
<div class="section-header"><h3>{{ t.planner_section_opex }}</h3></div>
|
||||||
{% else %}<div class="section-header"><h3>Monthly Operating Costs</h3></div>{% endif %}
|
|
||||||
|
|
||||||
<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="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>
|
<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>
|
||||||
|
|
||||||
<div class="mb-section">
|
<div class="mb-section">
|
||||||
{% if lang == 'de' %}<div class="section-header"><h3>Finanzierung</h3></div>
|
<div class="section-header"><h3>{{ t.planner_section_financing }}</h3></div>
|
||||||
{% else %}<div class="section-header"><h3>Financing</h3></div>{% endif %}
|
|
||||||
{{ slider('loanPct', t.sl_loan_pct, 0, 100, 1, s.loanPct, t.tip_loan_pct) }}
|
{{ 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('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) }}
|
{{ slider('loanTerm', t.sl_loan_term, 0, 30, 1, s.loanTerm, t.tip_loan_term) }}
|
||||||
@@ -345,8 +297,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-section">
|
<div class="mb-section">
|
||||||
{% if lang == 'de' %}<div class="section-header"><h3>Exit-Annahmen</h3></div>
|
<div class="section-header"><h3>{{ t.planner_section_exit }}</h3></div>
|
||||||
{% else %}<div class="section-header"><h3>Exit Assumptions</h3></div>{% endif %}
|
|
||||||
{{ slider('holdYears', t.sl_hold_years, 1, 20, 1, s.holdYears, t.tip_hold_years) }}
|
{{ 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('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) }}
|
{{ 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" %}
|
{% 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 %}
|
{% 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 name="description" content="{{ t.about_meta_desc }}">
|
||||||
<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:title" content="{{ t.about_page_title }}">
|
||||||
<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 property="og:description" content="{{ t.about_og_desc }}">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main class="container-page py-12">
|
<main class="container-page py-12">
|
||||||
<div class="card max-w-3xl mx-auto">
|
<div class="card max-w-3xl mx-auto">
|
||||||
<h1 class="text-2xl mb-6 text-center">
|
<h1 class="text-2xl mb-6 text-center">{{ t.about_h1 }}</h1>
|
||||||
{% if lang == 'de' %}Über {% endif %}{{ config.APP_NAME }}
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div class="space-y-4 text-slate-dark leading-relaxed">
|
<div class="space-y-4 text-slate-dark leading-relaxed">
|
||||||
{% if lang == 'de' %}
|
<p>{{ t.about_body_p1 }}</p>
|
||||||
<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>{{ t.about_body_p2 }}</p>
|
||||||
|
<p>{{ t.about_body_p3 }}</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>
|
|
||||||
|
|
||||||
<h3 class="text-lg mt-6">{{ t.about_why_h3 }}</h3>
|
<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>
|
<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>
|
<p>{{ t.about_next_p }}</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 %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-center mt-10">
|
<div class="text-center mt-10">
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{% extends "base.html" %}
|
{% 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 %}
|
{% 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 name="description" content="{{ t.features_meta_desc }}">
|
||||||
<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:title" content="{{ t.features_title_prefix }} | {{ config.APP_NAME }}">
|
||||||
<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 property="og:description" content="{{ t.features_meta_desc }}">
|
||||||
<meta property="og:image" content="{{ url_for('static', filename='images/planner-screenshot.png', _external=True) }}">
|
<meta property="og:image" content="{{ url_for('static', filename='images/planner-screenshot.png', _external=True) }}">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -19,112 +19,52 @@
|
|||||||
<div class="grid-2">
|
<div class="grid-2">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2 class="text-xl mb-2">{{ t.features_card_1_h2 }}</h2>
|
<h2 class="text-xl mb-2">{{ t.features_card_1_h2 }}</h2>
|
||||||
<p class="text-slate-dark">
|
<p class="text-slate-dark">{{ t.features_card_1_body }}</p>
|
||||||
{% 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>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2 class="text-xl mb-2">{{ t.features_card_2_h2 }}</h2>
|
<h2 class="text-xl mb-2">{{ t.features_card_2_h2 }}</h2>
|
||||||
<p class="text-slate-dark">
|
<p class="text-slate-dark">{{ t.features_card_2_body }}</p>
|
||||||
{% 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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid-2">
|
<div class="grid-2">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2 class="text-xl mb-2">{{ t.features_card_3_h2 }}</h2>
|
<h2 class="text-xl mb-2">{{ t.features_card_3_h2 }}</h2>
|
||||||
<p class="text-slate-dark">
|
<p class="text-slate-dark">{{ t.features_card_3_body }}</p>
|
||||||
{% 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>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2 class="text-xl mb-2">{{ t.features_card_4_h2 }}</h2>
|
<h2 class="text-xl mb-2">{{ t.features_card_4_h2 }}</h2>
|
||||||
<p class="text-slate-dark">
|
<p class="text-slate-dark">{{ t.features_card_4_body }}</p>
|
||||||
{% 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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid-2">
|
<div class="grid-2">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2 class="text-xl mb-2">{{ t.features_card_5_h2 }}</h2>
|
<h2 class="text-xl mb-2">{{ t.features_card_5_h2 }}</h2>
|
||||||
<p class="text-slate-dark">
|
<p class="text-slate-dark">{{ t.features_card_5_body }}</p>
|
||||||
{% 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>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2 class="text-xl mb-2">{{ t.features_card_6_h2 }}</h2>
|
<h2 class="text-xl mb-2">{{ t.features_card_6_h2 }}</h2>
|
||||||
<p class="text-slate-dark">
|
<p class="text-slate-dark">{{ t.features_card_6_body }}</p>
|
||||||
{% 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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-6 max-w-3xl mx-auto mt-12">
|
<div class="space-y-6 max-w-3xl mx-auto mt-12">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2 class="text-xl mb-2">{{ t.features_capex_h2 }}</h2>
|
<h2 class="text-xl mb-2">{{ t.features_capex_h2 }}</h2>
|
||||||
<p class="text-slate-dark">
|
<p class="text-slate-dark">{{ t.features_capex_body }}</p>
|
||||||
{% 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>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2 class="text-xl mb-2">{{ t.features_opex_h2 }}</h2>
|
<h2 class="text-xl mb-2">{{ t.features_opex_h2 }}</h2>
|
||||||
<p class="text-slate-dark">
|
<p class="text-slate-dark">{{ t.features_opex_body }}</p>
|
||||||
{% 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>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2 class="text-xl mb-2">{{ t.features_cf_h2 }}</h2>
|
<h2 class="text-xl mb-2">{{ t.features_cf_h2 }}</h2>
|
||||||
<p class="text-slate-dark">
|
<p class="text-slate-dark">{{ t.features_cf_body }}</p>
|
||||||
{% 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>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2 class="text-xl mb-2">{{ t.features_returns_h2 }}</h2>
|
<h2 class="text-xl mb-2">{{ t.features_returns_h2 }}</h2>
|
||||||
<p class="text-slate-dark">
|
<p class="text-slate-dark">{{ t.features_returns_body }}</p>
|
||||||
{% 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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{% extends "base.html" %}
|
{% 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 %}
|
{% 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 name="description" content="{{ t.landing_meta_desc }}">
|
||||||
<meta property="og:title" content="Padelnomics - Padel Court Financial Planner">
|
<meta property="og:title" content="{{ t.landing_page_title }}">
|
||||||
<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 property="og:description" content="{{ t.landing_og_desc }}">
|
||||||
<meta property="og:type" content="website">
|
<meta property="og:type" content="website">
|
||||||
<meta property="og:image" content="{{ url_for('static', filename='images/planner-screenshot.png', _external=True) }}">
|
<meta property="og:image" content="{{ url_for('static', filename='images/planner-screenshot.png', _external=True) }}">
|
||||||
<style>
|
<style>
|
||||||
@@ -250,14 +250,7 @@
|
|||||||
{{ t.landing_hero_h1_2 }}<br>
|
{{ t.landing_hero_h1_2 }}<br>
|
||||||
<span class="accent">{{ t.landing_hero_h1_3 }}</span>
|
<span class="accent">{{ t.landing_hero_h1_3 }}</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p class="hero-desc">
|
<p class="hero-desc">{{ t.landing_hero_desc }}</p>
|
||||||
{% 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>
|
|
||||||
<div class="hero-actions">
|
<div class="hero-actions">
|
||||||
<a href="{{ url_for('planner.index') }}" class="btn-hero">{{ t.landing_hero_btn_primary }}</a>
|
<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>
|
<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 journey-step--upcoming">
|
||||||
<div class="journey-step__num">01</div>
|
<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>
|
<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">
|
<p class="journey-step__desc">{{ t.landing_journey_01_desc }}</p>
|
||||||
{% if lang == 'de' %}
|
|
||||||
Marktbedarfsanalyse, Standortbewertung und Identifikation von Nachfragepotenzialen.
|
|
||||||
{% else %}
|
|
||||||
Market demand analysis, whitespace mapping, location scoring.
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="journey-step journey-step--active">
|
<div class="journey-step journey-step--active">
|
||||||
<div class="journey-step__num">02</div>
|
<div class="journey-step__num">02</div>
|
||||||
<h3 class="journey-step__title">{{ t.landing_journey_02 }}</h3>
|
<h3 class="journey-step__title">{{ t.landing_journey_02 }}</h3>
|
||||||
<p class="journey-step__desc">
|
<p class="journey-step__desc">{{ t.landing_journey_02_desc }}</p>
|
||||||
{% 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>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="journey-step journey-step--upcoming">
|
<div class="journey-step journey-step--upcoming">
|
||||||
<div class="journey-step__num">03</div>
|
<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>
|
<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">
|
<p class="journey-step__desc">{{ t.landing_journey_03_desc }}</p>
|
||||||
{% 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>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="journey-step journey-step--active">
|
<div class="journey-step journey-step--active">
|
||||||
<div class="journey-step__num">04</div>
|
<div class="journey-step__num">04</div>
|
||||||
<h3 class="journey-step__title">{{ t.landing_journey_04 }}</h3>
|
<h3 class="journey-step__title">{{ t.landing_journey_04 }}</h3>
|
||||||
<p class="journey-step__desc">
|
<p class="journey-step__desc">{{ t.landing_journey_04_desc | tformat(total_suppliers=total_suppliers, total_countries=total_countries) }}</p>
|
||||||
{% 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>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="journey-step journey-step--upcoming">
|
<div class="journey-step journey-step--upcoming">
|
||||||
<div class="journey-step__num">05</div>
|
<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>
|
<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">
|
<p class="journey-step__desc">{{ t.landing_journey_05_desc }}</p>
|
||||||
{% if lang == 'de' %}
|
|
||||||
Launch-Playbook, Performance-Benchmarks und Wachstumsanalysen für deinen Betrieb.
|
|
||||||
{% else %}
|
|
||||||
Launch playbook, performance benchmarks, and expansion analytics.
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -380,65 +343,29 @@
|
|||||||
<div class="grid-3">
|
<div class="grid-3">
|
||||||
<div class="card border-l-4 border-l-electric">
|
<div class="card border-l-4 border-l-electric">
|
||||||
<h3 class="text-lg mb-2">🔧 {{ t.landing_feature_1_h3 }}</h3>
|
<h3 class="text-lg mb-2">🔧 {{ t.landing_feature_1_h3 }}</h3>
|
||||||
<p class="text-sm text-slate-dark">
|
<p class="text-sm text-slate-dark">{{ t.landing_feature_1_body }}</p>
|
||||||
{% 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>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card border-l-4 border-l-accent">
|
<div class="card border-l-4 border-l-accent">
|
||||||
<h3 class="text-lg mb-2">📋 {{ t.landing_feature_2_h3 }}</h3>
|
<h3 class="text-lg mb-2">📋 {{ t.landing_feature_2_h3 }}</h3>
|
||||||
<p class="text-sm text-slate-dark">
|
<p class="text-sm text-slate-dark">{{ t.landing_feature_2_body }}</p>
|
||||||
{% 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>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card border-l-4 border-l-warning">
|
<div class="card border-l-4 border-l-warning">
|
||||||
<h3 class="text-lg mb-2">☀️ {{ t.landing_feature_3_h3 }}</h3>
|
<h3 class="text-lg mb-2">☀️ {{ t.landing_feature_3_h3 }}</h3>
|
||||||
<p class="text-sm text-slate-dark">
|
<p class="text-sm text-slate-dark">{{ t.landing_feature_3_body }}</p>
|
||||||
{% 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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid-3 mt-0">
|
<div class="grid-3 mt-0">
|
||||||
<div class="card border-l-4 border-l-danger">
|
<div class="card border-l-4 border-l-danger">
|
||||||
<h3 class="text-lg mb-2">📉 {{ t.landing_feature_4_h3 }}</h3>
|
<h3 class="text-lg mb-2">📉 {{ t.landing_feature_4_h3 }}</h3>
|
||||||
<p class="text-sm text-slate-dark">
|
<p class="text-sm text-slate-dark">{{ t.landing_feature_4_body }}</p>
|
||||||
{% 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>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card border-l-4 border-l-electric">
|
<div class="card border-l-4 border-l-electric">
|
||||||
<h3 class="text-lg mb-2">🎯 {{ t.landing_feature_5_h3 }}</h3>
|
<h3 class="text-lg mb-2">🎯 {{ t.landing_feature_5_h3 }}</h3>
|
||||||
<p class="text-sm text-slate-dark">
|
<p class="text-sm text-slate-dark">{{ t.landing_feature_5_body }}</p>
|
||||||
{% 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>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card border-l-4 border-l-accent">
|
<div class="card border-l-4 border-l-accent">
|
||||||
<h3 class="text-lg mb-2">💾 {{ t.landing_feature_6_h3 }}</h3>
|
<h3 class="text-lg mb-2">💾 {{ t.landing_feature_6_h3 }}</h3>
|
||||||
<p class="text-sm text-slate-dark">
|
<p class="text-sm text-slate-dark">{{ t.landing_feature_6_body }}</p>
|
||||||
{% 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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -446,46 +373,22 @@
|
|||||||
<!-- Supplier Matching -->
|
<!-- Supplier Matching -->
|
||||||
<section class="py-12">
|
<section class="py-12">
|
||||||
<h2 class="text-2xl text-center mb-2">{{ t.landing_supplier_title }}</h2>
|
<h2 class="text-2xl text-center mb-2">{{ t.landing_supplier_title }}</h2>
|
||||||
<p class="text-center text-slate mb-8">
|
<p class="text-center text-slate mb-8">{{ t.landing_supplier_sub | tformat(total_suppliers=total_suppliers, total_countries=total_countries) }}</p>
|
||||||
{% 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>
|
|
||||||
<div class="match-grid">
|
<div class="match-grid">
|
||||||
<div class="match-step">
|
<div class="match-step">
|
||||||
<div class="match-step__num">1</div>
|
<div class="match-step__num">1</div>
|
||||||
<h3>{{ t.landing_supplier_step_1_title }}</h3>
|
<h3>{{ t.landing_supplier_step_1_title }}</h3>
|
||||||
<p>
|
<p>{{ t.landing_supplier_step_1_body }}</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>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="match-step">
|
<div class="match-step">
|
||||||
<div class="match-step__num">2</div>
|
<div class="match-step__num">2</div>
|
||||||
<h3>{{ t.landing_supplier_step_2_title }}</h3>
|
<h3>{{ t.landing_supplier_step_2_title }}</h3>
|
||||||
<p>
|
<p>{{ t.landing_supplier_step_2_body }}</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>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="match-step">
|
<div class="match-step">
|
||||||
<div class="match-step__num">3</div>
|
<div class="match-step__num">3</div>
|
||||||
<h3>{{ t.landing_supplier_step_3_title }}</h3>
|
<h3>{{ t.landing_supplier_step_3_title }}</h3>
|
||||||
<p>
|
<p>{{ t.landing_supplier_step_3_body }}</p>
|
||||||
{% if lang == 'de' %}
|
|
||||||
Angebote von vermittelten Anbietern erhalten. Keine Kaltakquise erforderlich.
|
|
||||||
{% else %}
|
|
||||||
Receive proposals from matched suppliers. No cold outreach needed.
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center mt-8">
|
<div class="text-center mt-8">
|
||||||
@@ -499,53 +402,23 @@
|
|||||||
<div class="faq">
|
<div class="faq">
|
||||||
<details>
|
<details>
|
||||||
<summary>{{ t.landing_faq_q1 }}</summary>
|
<summary>{{ t.landing_faq_q1 }}</summary>
|
||||||
<p>
|
<p>{{ t.landing_faq_a1 }}</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>
|
|
||||||
</details>
|
</details>
|
||||||
<details>
|
<details>
|
||||||
<summary>{{ t.landing_faq_q2 }}</summary>
|
<summary>{{ t.landing_faq_q2 }}</summary>
|
||||||
<p>
|
<p>{{ t.landing_faq_a2 }}</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>
|
|
||||||
</details>
|
</details>
|
||||||
<details>
|
<details>
|
||||||
<summary>{{ t.landing_faq_q3 }}</summary>
|
<summary>{{ t.landing_faq_q3 }}</summary>
|
||||||
<p>
|
<p>{{ t.landing_faq_a3 }}</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>
|
|
||||||
</details>
|
</details>
|
||||||
<details>
|
<details>
|
||||||
<summary>{{ t.landing_faq_q4 }}</summary>
|
<summary>{{ t.landing_faq_q4 }}</summary>
|
||||||
<p>
|
<p>{{ t.landing_faq_a4 }}</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>
|
|
||||||
</details>
|
</details>
|
||||||
<details>
|
<details>
|
||||||
<summary>{{ t.landing_faq_q5 }}</summary>
|
<summary>{{ t.landing_faq_q5 }}</summary>
|
||||||
<p>
|
<p>{{ t.landing_faq_a5 }}</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>
|
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -554,21 +427,8 @@
|
|||||||
<section class="py-12 max-w-3xl mx-auto">
|
<section class="py-12 max-w-3xl mx-auto">
|
||||||
<h2 class="text-2xl mb-4">{{ t.landing_seo_title }}</h2>
|
<h2 class="text-2xl mb-4">{{ t.landing_seo_title }}</h2>
|
||||||
<div class="space-y-4 text-slate-dark leading-relaxed">
|
<div class="space-y-4 text-slate-dark leading-relaxed">
|
||||||
{% if lang == 'de' %}
|
<p>{{ t.landing_seo_p1 }}</p>
|
||||||
<p>
|
<p>{{ t.landing_seo_p2 }}</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 %}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -576,13 +436,7 @@
|
|||||||
<section style="padding: 2rem 0 4rem">
|
<section style="padding: 2rem 0 4rem">
|
||||||
<div class="cta-card">
|
<div class="cta-card">
|
||||||
<h2>{{ t.landing_final_cta_h2 }}</h2>
|
<h2>{{ t.landing_final_cta_h2 }}</h2>
|
||||||
<p>
|
<p>{{ t.landing_final_cta_sub | tformat(total_countries=total_countries) }}</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>
|
|
||||||
<a href="{{ url_for('planner.index') }}" class="cta-card__btn">{{ t.landing_final_cta_btn }}</a>
|
<a href="{{ url_for('planner.index') }}" class="cta-card__btn">{{ t.landing_final_cta_btn }}</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -656,7 +510,6 @@
|
|||||||
update();
|
update();
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
{% if lang == 'de' %}
|
|
||||||
<script type="application/ld+json">
|
<script type="application/ld+json">
|
||||||
{
|
{
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
@@ -664,7 +517,7 @@
|
|||||||
"name": "Padelnomics",
|
"name": "Padelnomics",
|
||||||
"url": "{{ config.BASE_URL }}",
|
"url": "{{ config.BASE_URL }}",
|
||||||
"logo": "{{ url_for('static', filename='images/logo.png', _external=True) }}",
|
"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>
|
||||||
<script type="application/ld+json">
|
<script type="application/ld+json">
|
||||||
@@ -674,105 +527,45 @@
|
|||||||
"mainEntity": [
|
"mainEntity": [
|
||||||
{
|
{
|
||||||
"@type": "Question",
|
"@type": "Question",
|
||||||
"name": "Was berechnet der Planer?",
|
"name": "{{ t.landing_faq_q1 }}",
|
||||||
"acceptedAnswer": {
|
"acceptedAnswer": {
|
||||||
"@type": "Answer",
|
"@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",
|
"@type": "Question",
|
||||||
"name": "Muss ich mich registrieren?",
|
"name": "{{ t.landing_faq_q2 }}",
|
||||||
"acceptedAnswer": {
|
"acceptedAnswer": {
|
||||||
"@type": "Answer",
|
"@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",
|
"@type": "Question",
|
||||||
"name": "Wie funktioniert die Anbieter-Vermittlung?",
|
"name": "{{ t.landing_faq_q3 }}",
|
||||||
"acceptedAnswer": {
|
"acceptedAnswer": {
|
||||||
"@type": "Answer",
|
"@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",
|
"@type": "Question",
|
||||||
"name": "Ist das Anbieterverzeichnis kostenlos?",
|
"name": "{{ t.landing_faq_q4 }}",
|
||||||
"acceptedAnswer": {
|
"acceptedAnswer": {
|
||||||
"@type": "Answer",
|
"@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",
|
"@type": "Question",
|
||||||
"name": "Wie genau sind die Finanzprojektionen?",
|
"name": "{{ t.landing_faq_q5 }}",
|
||||||
"acceptedAnswer": {
|
"acceptedAnswer": {
|
||||||
"@type": "Answer",
|
"@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>
|
</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 %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from ..core import (
|
|||||||
get_paddle_price,
|
get_paddle_price,
|
||||||
waitlist_gate,
|
waitlist_gate,
|
||||||
)
|
)
|
||||||
|
from ..i18n import get_translations
|
||||||
|
|
||||||
bp = Blueprint(
|
bp = Blueprint(
|
||||||
"suppliers",
|
"suppliers",
|
||||||
@@ -40,23 +41,14 @@ PLAN_FEATURES = {
|
|||||||
"monthly_credits": 0,
|
"monthly_credits": 0,
|
||||||
"paddle_key_monthly": "supplier_basic_monthly",
|
"paddle_key_monthly": "supplier_basic_monthly",
|
||||||
"paddle_key_yearly": "supplier_basic_yearly",
|
"paddle_key_yearly": "supplier_basic_yearly",
|
||||||
"features": [
|
"feature_keys": [
|
||||||
"Verified badge",
|
"plan_basic_f1",
|
||||||
"Company logo",
|
"plan_basic_f2",
|
||||||
"Full description & tagline",
|
"plan_basic_f3",
|
||||||
"Website & contact details shown",
|
"plan_basic_f4",
|
||||||
"Services offered checklist",
|
"plan_basic_f5",
|
||||||
"Social links (LinkedIn, Instagram, YouTube)",
|
"plan_basic_f6",
|
||||||
"Enquiry form on listing page",
|
"plan_basic_f7",
|
||||||
],
|
|
||||||
"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",
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
"supplier_growth": {
|
"supplier_growth": {
|
||||||
@@ -67,17 +59,11 @@ PLAN_FEATURES = {
|
|||||||
"monthly_credits": 30,
|
"monthly_credits": 30,
|
||||||
"paddle_key_monthly": "supplier_growth",
|
"paddle_key_monthly": "supplier_growth",
|
||||||
"paddle_key_yearly": "supplier_growth_yearly",
|
"paddle_key_yearly": "supplier_growth_yearly",
|
||||||
"features": [
|
"feature_keys": [
|
||||||
"Everything in Basic",
|
"plan_growth_f1",
|
||||||
"30 lead credits/month",
|
"plan_growth_f2",
|
||||||
"Lead feed access",
|
"plan_growth_f3",
|
||||||
"Priority over Basic listings",
|
"plan_growth_f4",
|
||||||
],
|
|
||||||
"features_de": [
|
|
||||||
"Alles aus Basic",
|
|
||||||
"30 Lead-Credits/Monat",
|
|
||||||
"Zugang zum Lead-Feed",
|
|
||||||
"Priorität gegenüber Basic-Einträgen",
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
"supplier_pro": {
|
"supplier_pro": {
|
||||||
@@ -89,28 +75,21 @@ PLAN_FEATURES = {
|
|||||||
"paddle_key_monthly": "supplier_pro",
|
"paddle_key_monthly": "supplier_pro",
|
||||||
"paddle_key_yearly": "supplier_pro_yearly",
|
"paddle_key_yearly": "supplier_pro_yearly",
|
||||||
"includes": ["logo", "highlight", "verified"],
|
"includes": ["logo", "highlight", "verified"],
|
||||||
"features": [
|
"feature_keys": [
|
||||||
"Everything in Growth",
|
"plan_pro_f1",
|
||||||
"100 lead credits/month",
|
"plan_pro_f2",
|
||||||
"Company logo displayed",
|
"plan_pro_f3",
|
||||||
"Highlighted card border",
|
"plan_pro_f4",
|
||||||
"Priority placement",
|
"plan_pro_f5",
|
||||||
],
|
|
||||||
"features_de": [
|
|
||||||
"Alles aus Growth",
|
|
||||||
"100 Lead-Credits/Monat",
|
|
||||||
"Firmenlogo angezeigt",
|
|
||||||
"Hervorgehobener Kartenrahmen",
|
|
||||||
"Bevorzugte Platzierung",
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
BOOST_OPTIONS = [
|
BOOST_OPTIONS = [
|
||||||
{"key": "boost_logo", "type": "logo", "name": "Logo", "price": 29, "desc": "Display your company logo"},
|
{"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": "Highlight", "price": 39, "desc": "Blue highlighted card border"},
|
{"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": "Verified Badge", "price": 49, "desc": "Verified checkmark badge"},
|
{"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": "Custom Card Color", "price": 19, "desc": "Stand out with a custom border color on your directory listing"},
|
{"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 = [
|
CREDIT_PACK_OPTIONS = [
|
||||||
@@ -158,15 +137,16 @@ def _supplier_required(f):
|
|||||||
|
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
async def decorated(*args, **kwargs):
|
async def decorated(*args, **kwargs):
|
||||||
|
t = get_translations(g.get("lang") or "en")
|
||||||
if not g.get("user"):
|
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))
|
return redirect(url_for("auth.login", next=request.path))
|
||||||
supplier = await fetch_one(
|
supplier = await fetch_one(
|
||||||
"SELECT * FROM suppliers WHERE claimed_by = ? AND tier IN ('basic', 'growth', 'pro')",
|
"SELECT * FROM suppliers WHERE claimed_by = ? AND tier IN ('basic', 'growth', 'pro')",
|
||||||
(g.user["id"],),
|
(g.user["id"],),
|
||||||
)
|
)
|
||||||
if not supplier:
|
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"))
|
return redirect(url_for("suppliers.signup"))
|
||||||
g.supplier = supplier
|
g.supplier = supplier
|
||||||
return await f(*args, **kwargs)
|
return await f(*args, **kwargs)
|
||||||
@@ -180,15 +160,16 @@ def _lead_tier_required(f):
|
|||||||
|
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
async def decorated(*args, **kwargs):
|
async def decorated(*args, **kwargs):
|
||||||
|
t = get_translations(g.get("lang") or "en")
|
||||||
if not g.get("user"):
|
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))
|
return redirect(url_for("auth.login", next=request.path))
|
||||||
supplier = await fetch_one(
|
supplier = await fetch_one(
|
||||||
"SELECT * FROM suppliers WHERE claimed_by = ? AND tier IN ('growth', 'pro')",
|
"SELECT * FROM suppliers WHERE claimed_by = ? AND tier IN ('growth', 'pro')",
|
||||||
(g.user["id"],),
|
(g.user["id"],),
|
||||||
)
|
)
|
||||||
if not supplier:
|
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"))
|
return redirect(url_for("suppliers.dashboard"))
|
||||||
g.supplier = supplier
|
g.supplier = supplier
|
||||||
return await f(*args, **kwargs)
|
return await f(*args, **kwargs)
|
||||||
@@ -239,7 +220,8 @@ async def signup_waitlist():
|
|||||||
plan = form.get("plan", "supplier_growth")
|
plan = form.get("plan", "supplier_growth")
|
||||||
|
|
||||||
if not email or "@" not in email:
|
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))
|
return redirect(url_for("suppliers.signup", plan=plan))
|
||||||
|
|
||||||
# Capture to DB with intent="supplier", but email confirmation uses plan name
|
# 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", [])
|
included_boosts = plan_info.get("includes", [])
|
||||||
|
|
||||||
# Compute order summary for step 4
|
# 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(
|
return await render_template(
|
||||||
f"suppliers/partials/signup_step_{next_step}.html",
|
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."""
|
"""Compute order summary from accumulated wizard state."""
|
||||||
plan = data.get("plan", "supplier_growth")
|
plan = data.get("plan", "supplier_growth")
|
||||||
plan_info = PLAN_FEATURES.get(plan, PLAN_FEATURES["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":
|
if period == "yearly":
|
||||||
plan_price = plan_info["yearly_price"]
|
plan_price = plan_info["yearly_price"]
|
||||||
plan_price_display = plan_info["yearly_monthly_equivalent"]
|
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:
|
else:
|
||||||
plan_price = plan_info["monthly_price"]
|
plan_price = plan_info["monthly_price"]
|
||||||
plan_price_display = plan_info["monthly_price"]
|
plan_price_display = plan_info["monthly_price"]
|
||||||
billing_label = "billed monthly"
|
billing_label = t["sd_billing_monthly"]
|
||||||
|
|
||||||
one_time = 0
|
one_time = 0
|
||||||
selected_boosts = data.get("boosts", [])
|
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,)
|
"SELECT * FROM suppliers WHERE slug = ? AND claimed_by IS NULL", (slug,)
|
||||||
)
|
)
|
||||||
if not supplier:
|
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("directory.index"))
|
||||||
return redirect(url_for("suppliers.signup", claim=slug))
|
return redirect(url_for("suppliers.signup", claim=slug))
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{% extends "base.html" %}
|
{% 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 paddle %}{% include "_paddle.html" %}{% endblock %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
hx-push-url="{{ url_for('suppliers.dashboard', tab='overview') }}"
|
hx-push-url="{{ url_for('suppliers.dashboard', tab='overview') }}"
|
||||||
class="{% if active_tab == 'overview' %}active{% endif %}">
|
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>
|
<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>
|
</a>
|
||||||
{% if supplier.tier in ('growth', 'pro') %}
|
{% if supplier.tier in ('growth', 'pro') %}
|
||||||
<a href="{{ url_for('suppliers.dashboard', tab='leads') }}"
|
<a href="{{ url_for('suppliers.dashboard', tab='leads') }}"
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
hx-push-url="{{ url_for('suppliers.dashboard', tab='leads') }}"
|
hx-push-url="{{ url_for('suppliers.dashboard', tab='leads') }}"
|
||||||
class="{% if active_tab == 'leads' %}active{% endif %}">
|
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>
|
<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>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{{ url_for('suppliers.dashboard', tab='listing') }}"
|
<a href="{{ url_for('suppliers.dashboard', tab='listing') }}"
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
hx-push-url="{{ url_for('suppliers.dashboard', tab='listing') }}"
|
hx-push-url="{{ url_for('suppliers.dashboard', tab='listing') }}"
|
||||||
class="{% if active_tab == 'listing' %}active{% endif %}">
|
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>
|
<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>
|
</a>
|
||||||
{% if supplier.tier in ('growth', 'pro') %}
|
{% if supplier.tier in ('growth', 'pro') %}
|
||||||
<a href="{{ url_for('suppliers.dashboard', tab='boosts') }}"
|
<a href="{{ url_for('suppliers.dashboard', tab='boosts') }}"
|
||||||
@@ -94,23 +94,23 @@
|
|||||||
hx-push-url="{{ url_for('suppliers.dashboard', tab='boosts') }}"
|
hx-push-url="{{ url_for('suppliers.dashboard', tab='boosts') }}"
|
||||||
class="{% if active_tab == 'boosts' %}active{% endif %}">
|
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>
|
<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>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{% if supplier.tier == 'basic' %}
|
{% 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">
|
<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">
|
<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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="dash-sidebar__footer">
|
<div class="dash-sidebar__footer">
|
||||||
<div class="dash-sidebar__credits" id="sidebar-credits">
|
<div class="dash-sidebar__credits" id="sidebar-credits">
|
||||||
{{ supplier.credit_balance }} credits
|
{{ supplier.credit_balance }} {{ t.sd_credits }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</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-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-trigger="load"
|
||||||
hx-swap="innerHTML">
|
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>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}Lead Feed - {{ config.APP_NAME }}{% endblock %}
|
{% block title %}{{ t.sd_lf_page_title }} - {{ config.APP_NAME }}{% endblock %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<style>
|
<style>
|
||||||
@@ -49,23 +49,23 @@
|
|||||||
<main class="container-page py-12">
|
<main class="container-page py-12">
|
||||||
<div class="lf-header">
|
<div class="lf-header">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-2xl">Lead Feed</h1>
|
<h1 class="text-2xl">{{ t.sd_lf_h1 }}</h1>
|
||||||
<p class="text-sm text-slate">Browse and unlock qualified padel project leads.</p>
|
<p class="text-sm text-slate">{{ t.sd_lf_subtitle }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="lf-balance">
|
<div class="lf-balance">
|
||||||
{{ supplier.credit_balance }} credits available
|
{{ supplier.credit_balance }} {{ t.sd_lf_credits_available }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="lf-filters">
|
<div class="lf-filters">
|
||||||
<select onchange="window.location='?country='+this.value+'&heat={{ current_heat }}'" class="form-input" style="min-width:120px">
|
<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 %}
|
{% for c in countries %}
|
||||||
<option value="{{ c }}" {% if c == current_country %}selected{% endif %}>{{ c }}</option>
|
<option value="{{ c }}" {% if c == current_country %}selected{% endif %}>{{ c }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
<select onchange="window.location='?country={{ current_country }}&heat='+this.value" class="form-input" style="min-width:100px">
|
<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="hot" {% if current_heat == 'hot' %}selected{% endif %}>HOT</option>
|
||||||
<option value="warm" {% if current_heat == 'warm' %}selected{% endif %}>WARM</option>
|
<option value="warm" {% if current_heat == 'warm' %}selected{% endif %}>WARM</option>
|
||||||
<option value="cool" {% if current_heat == 'cool' %}selected{% endif %}>COOL</option>
|
<option value="cool" {% if current_heat == 'cool' %}selected{% endif %}>COOL</option>
|
||||||
@@ -86,8 +86,8 @@
|
|||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="card text-center" style="padding:3rem">
|
<div class="card text-center" style="padding:3rem">
|
||||||
<h3 style="color:#64748B">No leads match your filters</h3>
|
<h3 style="color:#64748B">{{ t.sd_lf_no_match }}</h3>
|
||||||
<p style="color:#94A3B8;font-size:0.875rem">Try adjusting your country or heat filters, or check back later for new leads.</p>
|
<p style="color:#94A3B8;font-size:0.875rem">{{ t.sd_lf_no_match_hint }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -56,57 +56,57 @@
|
|||||||
<div style="max-width:720px">
|
<div style="max-width:720px">
|
||||||
<!-- Current Plan -->
|
<!-- Current Plan -->
|
||||||
<div class="bst-section">
|
<div class="bst-section">
|
||||||
<h3>Current Plan</h3>
|
<h3>{{ t.sd_bst_current_plan }}</h3>
|
||||||
<div class="bst-plan">
|
<div class="bst-plan">
|
||||||
<div>
|
<div>
|
||||||
<div class="bst-plan__name">{{ plan_info.name }}</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>
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Active Boosts -->
|
<!-- Active Boosts -->
|
||||||
<div class="bst-section">
|
<div class="bst-section">
|
||||||
<h3>Active Boosts</h3>
|
<h3>{{ t.sd_bst_active_boosts }}</h3>
|
||||||
{% if active_boosts %}
|
{% if active_boosts %}
|
||||||
{% for boost in active_boosts %}
|
{% for boost in active_boosts %}
|
||||||
<div class="bst-boost-card bst-boost-card--active">
|
<div class="bst-boost-card bst-boost-card--active">
|
||||||
<div>
|
<div>
|
||||||
<div class="bst-boost__name">{{ boost.boost_type | replace('_', ' ') | title }}</div>
|
<div class="bst-boost__name">{{ boost.boost_type | replace('_', ' ') | title }}</div>
|
||||||
{% if boost.expires_at %}
|
{% 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 %}
|
{% else %}
|
||||||
<div class="bst-boost__desc">Active subscription</div>
|
<div class="bst-boost__desc">{{ t.sd_bst_active_subscription }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<span class="bst-boost__status">Active</span>
|
<span class="bst-boost__status">{{ t.sd_bst_active }}</span>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% 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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Available Boosts -->
|
<!-- Available Boosts -->
|
||||||
<div class="bst-section">
|
<div class="bst-section">
|
||||||
<h3>Available Boosts</h3>
|
<h3>{{ t.sd_bst_available_boosts }}</h3>
|
||||||
{% for b in boost_options %}
|
{% for b in boost_options %}
|
||||||
{% if b.type not in active_boost_types %}
|
{% if b.type not in active_boost_types %}
|
||||||
<div class="bst-boost-card">
|
<div class="bst-boost-card">
|
||||||
<div>
|
<div>
|
||||||
<div class="bst-boost__name">{{ b.name }}</div>
|
<div class="bst-boost__name">{{ t[b.name_key] }}</div>
|
||||||
<div class="bst-boost__desc">{{ b.desc }}</div>
|
<div class="bst-boost__desc">{{ t[b.desc_key] }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="text-align:right">
|
<div style="text-align:right">
|
||||||
<div class="bst-boost__price">€{{ b.price }}/mo</div>
|
<div class="bst-boost__price">€{{ b.price }}/mo</div>
|
||||||
{% if price_ids.get(b.key) %}
|
{% if price_ids.get(b.key) %}
|
||||||
<button type="button" class="bst-buy-btn"
|
<button type="button" class="bst-buy-btn"
|
||||||
onclick="Paddle.Checkout.open({items:[{priceId:'{{ price_ids[b.key] }}',quantity:1}],customData:{supplier_id:'{{ supplier.id }}'}})">
|
onclick="Paddle.Checkout.open({items:[{priceId:'{{ price_ids[b.key] }}',quantity:1}],customData:{supplier_id:'{{ supplier.id }}'}})">
|
||||||
Activate
|
{{ t.sd_bst_activate }}
|
||||||
</button>
|
</button>
|
||||||
{% else %}
|
{% 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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -116,20 +116,20 @@
|
|||||||
|
|
||||||
<!-- Credit Packs -->
|
<!-- Credit Packs -->
|
||||||
<div class="bst-section">
|
<div class="bst-section">
|
||||||
<h3>Buy Credit Packs</h3>
|
<h3>{{ t.sd_bst_buy_credits }}</h3>
|
||||||
<div class="bst-credits-grid">
|
<div class="bst-credits-grid">
|
||||||
{% for cp in credit_packs %}
|
{% for cp in credit_packs %}
|
||||||
<div class="bst-credit-card">
|
<div class="bst-credit-card">
|
||||||
<div class="bst-credit-card__amount">{{ cp.amount }}</div>
|
<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>
|
<div class="bst-credit-card__price">€{{ cp.price }}</div>
|
||||||
{% if price_ids.get(cp.key) %}
|
{% if price_ids.get(cp.key) %}
|
||||||
<button type="button" class="bst-buy-btn"
|
<button type="button" class="bst-buy-btn"
|
||||||
onclick="Paddle.Checkout.open({items:[{priceId:'{{ price_ids[cp.key] }}',quantity:1}],customData:{supplier_id:'{{ supplier.id }}'}})">
|
onclick="Paddle.Checkout.open({items:[{priceId:'{{ price_ids[cp.key] }}',quantity:1}],customData:{supplier_id:'{{ supplier.id }}'}})">
|
||||||
Buy
|
{{ t.sd_bst_buy }}
|
||||||
</button>
|
</button>
|
||||||
{% else %}
|
{% 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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -140,21 +140,21 @@
|
|||||||
<!-- Summary Sidebar -->
|
<!-- Summary Sidebar -->
|
||||||
<div class="bst-sidebar">
|
<div class="bst-sidebar">
|
||||||
<div class="bst-summary">
|
<div class="bst-summary">
|
||||||
<h3>Summary</h3>
|
<h3>{{ t.sd_bst_summary }}</h3>
|
||||||
<div class="bst-summary__row">
|
<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>
|
<span>€{{ plan_info.price }}/mo</span>
|
||||||
</div>
|
</div>
|
||||||
{% for boost in active_boosts %}
|
{% for boost in active_boosts %}
|
||||||
{% if not boost.expires_at %}
|
{% if not boost.expires_at %}
|
||||||
<div class="bst-summary__row">
|
<div class="bst-summary__row">
|
||||||
<span>{{ boost.boost_type | replace('_', ' ') | title }}</span>
|
<span>{{ boost.boost_type | replace('_', ' ') | title }}</span>
|
||||||
<span>subscription</span>
|
<span>{{ t.sd_bst_subscription }}</span>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<div class="bst-summary__row bst-summary__total">
|
<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>
|
<span style="color:#1D4ED8">{{ supplier.credit_balance }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -32,17 +32,17 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="dl-top">
|
<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">
|
<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') }}"
|
<a href="{{ url_for('suppliers.dashboard', tab='boosts') }}"
|
||||||
hx-get="{{ url_for('suppliers.dashboard_boosts') }}"
|
hx-get="{{ url_for('suppliers.dashboard_boosts') }}"
|
||||||
hx-target="#dashboard-content"
|
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>
|
||||||
</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 '' }}"
|
value="{{ current_q if current_q is defined else '' }}"
|
||||||
hx-get="{{ url_for('suppliers.dashboard_leads') }}"
|
hx-get="{{ url_for('suppliers.dashboard_leads') }}"
|
||||||
hx-trigger="input changed delay:300ms"
|
hx-trigger="input changed delay:300ms"
|
||||||
@@ -54,13 +54,13 @@
|
|||||||
hx-push-url="false">
|
hx-push-url="false">
|
||||||
<!-- Heat filters -->
|
<!-- Heat filters -->
|
||||||
<a class="dl-pill {% if not current_heat %}dl-pill--active{% endif %}"
|
<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 %}"
|
<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 %}"
|
<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 %}"
|
<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>
|
<div class="dl-sep"></div>
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
hx-target="#dashboard-content"
|
hx-target="#dashboard-content"
|
||||||
hx-include="[name='heat'],[name='timeline']"
|
hx-include="[name='heat'],[name='timeline']"
|
||||||
name="country">
|
name="country">
|
||||||
<option value="">All countries</option>
|
<option value="">{{ t.sd_leads_filter_countries }}</option>
|
||||||
{% for c in countries %}
|
{% for c in countries %}
|
||||||
<option value="{{ c }}" {% if c == current_country %}selected{% endif %}>{{ c }}</option>
|
<option value="{{ c }}" {% if c == current_country %}selected{% endif %}>{{ c }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -82,13 +82,13 @@
|
|||||||
|
|
||||||
<!-- Timeline filters -->
|
<!-- Timeline filters -->
|
||||||
<a class="dl-pill {% if not current_timeline %}dl-pill--active{% endif %}"
|
<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 %}"
|
<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 %}"
|
<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 %}"
|
<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>
|
</div>
|
||||||
|
|
||||||
{% if leads %}
|
{% if leads %}
|
||||||
@@ -103,27 +103,27 @@
|
|||||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.5rem">
|
<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__heat lf-card__heat--{{ lead.heat_score or 'cool' }}">{{ (lead.heat_score or 'cool') | upper }}</span>
|
||||||
{% if lead.country in service_area %}
|
{% 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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<dl class="lf-card__meta">
|
<dl class="lf-card__meta">
|
||||||
<dt>Facility</dt><dd>{{ lead.facility_type or '-' }}</dd>
|
<dt>{{ t.sd_leads_facility }}</dt><dd>{{ lead.facility_type or '-' }}</dd>
|
||||||
<dt>Courts</dt><dd>{{ lead.court_count or '-' }}</dd>
|
<dt>{{ t.sd_leads_courts }}</dt><dd>{{ lead.court_count or '-' }}</dd>
|
||||||
<dt>Country</dt><dd>{{ lead.country or '-' }}</dd>
|
<dt>{{ t.sd_leads_country }}</dt><dd>{{ lead.country or '-' }}</dd>
|
||||||
<dt>Timeline</dt><dd>{{ lead.timeline or '-' }}</dd>
|
<dt>{{ t.sd_leads_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_budget }}</dt><dd>{% if lead.budget_estimate %}€{{ lead.budget_estimate }}{% else %}-{% endif %}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
{# Bidder count messaging #}
|
{# Bidder count messaging #}
|
||||||
{% if lead.bidder_count == 0 %}
|
{% 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 %}
|
{% 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 %}
|
{% endif %}
|
||||||
<div class="lf-card__foot">
|
<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">
|
<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() }}">
|
<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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -133,8 +133,8 @@
|
|||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="card text-center" style="padding:3rem">
|
<div class="card text-center" style="padding:3rem">
|
||||||
<h3 style="color:#64748B">No leads match your filters</h3>
|
<h3 style="color:#64748B">{{ t.sd_leads_no_match }}</h3>
|
||||||
<p style="color:#94A3B8;font-size:0.875rem">Try adjusting your filters, or check back later for new leads.</p>
|
<p style="color:#94A3B8;font-size:0.875rem">{{ t.sd_leads_no_match_hint }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|||||||
@@ -43,12 +43,12 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
{% if saved is defined and saved %}
|
{% if saved is defined and saved %}
|
||||||
<div class="lst-saved">Listing saved successfully.</div>
|
<div class="lst-saved">{{ t.sd_lst_saved }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Listing Preview -->
|
<!-- Listing Preview -->
|
||||||
<div class="lst-preview">
|
<div class="lst-preview">
|
||||||
<h3>Your Directory Card Preview</h3>
|
<h3>{{ t.sd_lst_preview_title }}</h3>
|
||||||
<div id="lst-preview">
|
<div id="lst-preview">
|
||||||
{% include "suppliers/partials/dashboard_listing_preview.html" %}
|
{% include "suppliers/partials/dashboard_listing_preview.html" %}
|
||||||
</div>
|
</div>
|
||||||
@@ -56,53 +56,53 @@
|
|||||||
|
|
||||||
<!-- Edit Form -->
|
<!-- Edit Form -->
|
||||||
<div class="lst-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">
|
<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() }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
|
||||||
<div class="lst-row">
|
<div class="lst-row">
|
||||||
<div>
|
<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"
|
<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-get="{{ url_for('suppliers.dashboard_listing_preview') }}" hx-trigger="input changed delay:500ms"
|
||||||
hx-target="#lst-preview" hx-include="#lst-edit-form">
|
hx-target="#lst-preview" hx-include="#lst-edit-form">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="lst-label">Tagline</label>
|
<label class="lst-label">{{ t.sd_lst_tagline }}</label>
|
||||||
<input type="text" name="tagline" value="{{ supplier.tagline or '' }}" class="lst-input" placeholder="One-liner for search results"
|
<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-get="{{ url_for('suppliers.dashboard_listing_preview') }}" hx-trigger="input changed delay:500ms"
|
||||||
hx-target="#lst-preview" hx-include="#lst-edit-form">
|
hx-target="#lst-preview" hx-include="#lst-edit-form">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="lst-full">
|
<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"
|
<textarea name="short_description" class="lst-input lst-textarea"
|
||||||
hx-get="{{ url_for('suppliers.dashboard_listing_preview') }}" hx-trigger="input changed delay:500ms"
|
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>
|
hx-target="#lst-preview" hx-include="#lst-edit-form">{{ supplier.short_description or '' }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="lst-full">
|
<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>
|
<textarea name="long_description" class="lst-input lst-textarea" style="min-height:120px">{{ supplier.long_description or '' }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="lst-row">
|
<div class="lst-row">
|
||||||
<div>
|
<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://..."
|
<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-get="{{ url_for('suppliers.dashboard_listing_preview') }}" hx-trigger="input changed delay:500ms"
|
||||||
hx-target="#lst-preview" hx-include="#lst-edit-form">
|
hx-target="#lst-preview" hx-include="#lst-edit-form">
|
||||||
</div>
|
</div>
|
||||||
<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">
|
<input type="file" name="logo_file" accept="image/*" class="lst-input" style="padding:6px 8px">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="lst-full">
|
<div class="lst-full">
|
||||||
<label class="lst-label">Cover Photo
|
<label class="lst-label">{{ t.sd_lst_cover_photo }}
|
||||||
<span style="font-weight:400;color:#94A3B8"> — 16:9, min 640px wide. Shown in directory search results.</span>
|
<span style="font-weight:400;color:#94A3B8">{{ t.sd_lst_cover_hint }}</span>
|
||||||
</label>
|
</label>
|
||||||
{% if supplier.cover_image %}
|
{% 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">
|
<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 class="lst-row">
|
||||||
<div>
|
<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">
|
<input type="text" name="contact_name" value="{{ supplier.contact_name or '' }}" class="lst-input">
|
||||||
</div>
|
</div>
|
||||||
<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">
|
<input type="email" name="contact_email" value="{{ supplier.contact_email or '' }}" class="lst-input">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="lst-row">
|
<div class="lst-row">
|
||||||
<div>
|
<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">
|
<input type="tel" name="contact_phone" value="{{ supplier.contact_phone or '' }}" class="lst-input">
|
||||||
</div>
|
</div>
|
||||||
<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">
|
<input type="number" name="years_in_business" value="{{ supplier.years_in_business or '' }}" class="lst-input" min="0">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="lst-full">
|
<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">
|
<input type="number" name="project_count" value="{{ supplier.project_count or '' }}" class="lst-input" min="0" style="max-width:200px">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="lst-full">
|
<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">
|
<div class="lst-pills">
|
||||||
{% set current_cats = (supplier.service_categories or '').split(',') %}
|
{% set current_cats = (supplier.service_categories or '').split(',') %}
|
||||||
{% for cat in service_categories %}
|
{% for cat in service_categories %}
|
||||||
@@ -151,7 +151,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="lst-full" style="margin-top:0.75rem">
|
<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">
|
<div class="lst-pills">
|
||||||
{% set current_areas = (supplier.service_area or '').split(',') %}
|
{% set current_areas = (supplier.service_area or '').split(',') %}
|
||||||
{% for c in countries %}
|
{% for c in countries %}
|
||||||
@@ -165,7 +165,7 @@
|
|||||||
|
|
||||||
<div class="lst-row">
|
<div class="lst-row">
|
||||||
<div>
|
<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"
|
<input type="text" name="contact_role" value="{{ supplier.contact_role or '' }}" class="lst-input"
|
||||||
placeholder="e.g. International Sales, Managing Director">
|
placeholder="e.g. International Sales, Managing Director">
|
||||||
</div>
|
</div>
|
||||||
@@ -173,7 +173,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="lst-full" style="margin-top:0.25rem">
|
<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">
|
<div class="lst-pills">
|
||||||
{% set current_services = (supplier.services_offered or '').split(',') %}
|
{% set current_services = (supplier.services_offered or '').split(',') %}
|
||||||
{% set service_options = [
|
{% set service_options = [
|
||||||
@@ -199,7 +199,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="lst-full" style="margin-top:0.75rem">
|
<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">
|
<div style="display:flex;flex-direction:column;gap:0.5rem">
|
||||||
<input type="url" name="linkedin_url" value="{{ supplier.linkedin_url or '' }}" class="lst-input"
|
<input type="url" name="linkedin_url" value="{{ supplier.linkedin_url or '' }}" class="lst-input"
|
||||||
placeholder="https://linkedin.com/company/...">
|
placeholder="https://linkedin.com/company/...">
|
||||||
@@ -211,7 +211,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-top:1.5rem">
|
<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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<div class="lst-badges">
|
<div class="lst-badges">
|
||||||
<span class="lst-badge lst-badge--tier">{{ supplier.tier | upper }}</span>
|
<span class="lst-badge lst-badge--tier">{{ supplier.tier | upper }}</span>
|
||||||
{% if 'verified' in active_boosts or supplier.is_verified %}
|
{% 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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if supplier.tagline %}
|
{% if supplier.tagline %}
|
||||||
|
|||||||
@@ -33,53 +33,53 @@
|
|||||||
{% if supplier.tier in ('growth', 'pro') and new_leads_count > 0 %}
|
{% if supplier.tier in ('growth', 'pro') and new_leads_count > 0 %}
|
||||||
<div class="ov-alert">
|
<div class="ov-alert">
|
||||||
<span class="ov-alert__count">{{ new_leads_count }}</span>
|
<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') }}"
|
<a href="{{ url_for('suppliers.dashboard', tab='leads') }}"
|
||||||
hx-get="{{ url_for('suppliers.dashboard_leads') }}"
|
hx-get="{{ url_for('suppliers.dashboard_leads') }}"
|
||||||
hx-target="#dashboard-content"
|
hx-target="#dashboard-content"
|
||||||
hx-push-url="{{ url_for('suppliers.dashboard', tab='leads') }}"
|
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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="ov-stats">
|
<div class="ov-stats">
|
||||||
<div class="ov-stat">
|
<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 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' %}
|
{% if supplier.tier == 'basic' %}
|
||||||
<div class="ov-stat">
|
<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 class="ov-stat__value">{{ enquiry_count }}</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="ov-stat">
|
<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 class="ov-stat__value">{{ leads_unlocked }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ov-stat">
|
<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 class="ov-stat__value ov-stat__value--blue">{{ supplier.credit_balance }}</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="ov-stat">
|
<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 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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if supplier.tier == 'basic' %}
|
{% 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">
|
<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.
|
<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">Upgrade to Growth →</a>
|
<a href="{{ url_for('suppliers.signup') }}" style="display:block;font-weight:600;margin-top:4px;color:#1D4ED8">{{ t.sd_ov_upgrade_growth }} →</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="ov-activity">
|
<div class="ov-activity">
|
||||||
<h3>Recent Activity</h3>
|
<h3>{{ t.sd_ov_recent_activity }}</h3>
|
||||||
{% if recent_activity %}
|
{% if recent_activity %}
|
||||||
{% for item in recent_activity %}
|
{% for item in recent_activity %}
|
||||||
<div class="ov-activity__item">
|
<div class="ov-activity__item">
|
||||||
@@ -88,11 +88,11 @@
|
|||||||
<span class="ov-activity__time">{{ item.created_at[:16] }}</span>
|
<span class="ov-activity__time">{{ item.created_at[:16] }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="ov-activity__delta {% if item.delta < 0 %}ov-activity__delta--neg{% else %}ov-activity__delta--pos{% endif %}">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% 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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,34 +1,34 @@
|
|||||||
<div class="lf-card">
|
<div class="lf-card">
|
||||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.5rem">
|
<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__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>
|
</div>
|
||||||
|
|
||||||
<dl class="lf-card__meta">
|
<dl class="lf-card__meta">
|
||||||
<dt>Facility</dt>
|
<dt>{{ t.sd_card_facility }}</dt>
|
||||||
<dd>{{ lead.facility_type or '-' }}</dd>
|
<dd>{{ lead.facility_type or '-' }}</dd>
|
||||||
<dt>Courts</dt>
|
<dt>{{ t.sd_card_courts }}</dt>
|
||||||
<dd>{{ lead.court_count or '-' }}</dd>
|
<dd>{{ lead.court_count or '-' }}</dd>
|
||||||
<dt>Country</dt>
|
<dt>{{ t.sd_card_country }}</dt>
|
||||||
<dd>{{ lead.country or '-' }}</dd>
|
<dd>{{ lead.country or '-' }}</dd>
|
||||||
<dt>Timeline</dt>
|
<dt>{{ t.sd_card_timeline }}</dt>
|
||||||
<dd>{{ lead.timeline or '-' }}</dd>
|
<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>
|
<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>
|
<dd>{{ lead.build_context or '-' }}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
{% if lead.services_needed %}
|
{% 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 %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="lf-card__foot">
|
<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) }}"
|
<form hx-post="{{ url_for('suppliers.unlock_lead', lead_id=lead.id) }}"
|
||||||
hx-target="#lead-card-{{ lead.id }}" hx-swap="innerHTML">
|
hx-target="#lead-card-{{ lead.id }}" hx-swap="innerHTML">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<div class="lf-card" style="border-color:#FCA5A5">
|
<div class="lf-card" style="border-color:#FCA5A5">
|
||||||
<div style="text-align:center;padding:0.75rem 0">
|
<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.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">You have <strong>{{ balance }}</strong> credits, this lead costs <strong>{{ required }}</strong>.</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">Buy Credits</a>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,84 +1,84 @@
|
|||||||
{# Human-readable labels for enum values #}
|
{# Human-readable labels for enum values — resolved from translation keys #}
|
||||||
{% set timeline_labels = {'asap': 'ASAP', '3_6_months': '3-6 months', '6_12_months': '6-12 months', '12_plus': '12+ months', 'exploring': 'Exploring'} %}
|
{% 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': '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 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': 'Self-funded', 'loan_approved': 'Loan approved', 'seeking': 'Seeking financing', 'not_started': 'Not started'} %}
|
{% 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': 'Solo decision-maker', 'partners': 'With partners', 'board': 'Board/committee', 'investor': 'Investor-led'} %}
|
{% 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': 'Has received quotes', 'contacted': 'Has contacted suppliers', 'none': 'No prior contact'} %}
|
{% set contact_labels = {'received_quotes': t.sd_contact_received_quotes, 'contacted': t.sd_contact_contacted, 'none': t.sd_contact_none} %}
|
||||||
{% set stakeholder_labels = {'owner': 'Owner/Operator', 'investor': 'Investor', 'developer': 'Property Developer', 'club': 'Club/Association', 'other': 'Other'} %}
|
{% 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 class="lf-card lf-card--unlocked">
|
||||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.75rem">
|
<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 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>
|
</div>
|
||||||
|
|
||||||
{# --- Project --- #}
|
{# --- Project --- #}
|
||||||
<div class="lf-section">
|
<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">
|
<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>
|
<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>
|
<dd>{{ lead.court_count or '-' }}</dd>
|
||||||
<dt>Glass</dt>
|
<dt>{{ t.sd_unlocked_label_glass }}</dt>
|
||||||
<dd>{{ lead.glass_type or '-' }}</dd>
|
<dd>{{ lead.glass_type or '-' }}</dd>
|
||||||
<dt>Lighting</dt>
|
<dt>{{ t.sd_unlocked_label_lighting }}</dt>
|
||||||
<dd>{{ lead.lighting_type or '-' }}</dd>
|
<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>
|
<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>
|
<dd>{{ lead.services_needed or '-' }}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# --- Location & Timeline --- #}
|
{# --- Location & Timeline --- #}
|
||||||
<div class="lf-section">
|
<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">
|
<dl class="lf-card__meta">
|
||||||
<dt>Location</dt>
|
<dt>{{ t.sd_unlocked_label_location }}</dt>
|
||||||
<dd>{{ lead.location or '-' }}, {{ lead.country or '-' }}</dd>
|
<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>
|
<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>
|
<dd>{{ phase_labels.get(lead.location_status, lead.location_status) or '-' }}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# --- Readiness --- #}
|
{# --- Readiness --- #}
|
||||||
<div class="lf-section">
|
<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">
|
<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>
|
<dd>{{ financing_labels.get(lead.financing_status, lead.financing_status) or '-' }}</dd>
|
||||||
<dt>Wants financing help</dt>
|
<dt>{{ t.sd_unlocked_label_wants_financing }}</dt>
|
||||||
<dd>{{ 'Yes' if lead.wants_financing_help else 'No' }}</dd>
|
<dd>{{ t.sd_unlocked_yes if lead.wants_financing_help else t.sd_unlocked_no }}</dd>
|
||||||
<dt>Decision process</dt>
|
<dt>{{ t.sd_unlocked_label_decision }}</dt>
|
||||||
<dd>{{ decision_labels.get(lead.decision_process, lead.decision_process) or '-' }}</dd>
|
<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>
|
<dd>{{ contact_labels.get(lead.previous_supplier_contact, lead.previous_supplier_contact) or '-' }}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if lead.additional_info %}
|
{% if lead.additional_info %}
|
||||||
<div class="lf-section">
|
<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>
|
<p style="font-size:0.75rem;color:#475569;background:#F8FAFC;padding:8px;border-radius:6px;margin:0">{{ lead.additional_info }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# --- Contact --- #}
|
{# --- Contact --- #}
|
||||||
<div class="lf-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">
|
<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>
|
<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>
|
<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>
|
<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>
|
<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>
|
<dd>{{ stakeholder_labels.get(lead.stakeholder_type, lead.stakeholder_type) or '-' }}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
@@ -88,26 +88,26 @@
|
|||||||
{% if sid %}
|
{% if sid %}
|
||||||
<a href="{{ url_for('planner.index') }}?scenario={{ sid }}" target="_blank"
|
<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">
|
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>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if credit_cost is defined %}
|
{% 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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if credit_cost is defined %}
|
{% if credit_cost is defined %}
|
||||||
{# OOB: update sidebar credits #}
|
{# OOB: update sidebar credits #}
|
||||||
<div id="sidebar-credits" hx-swap-oob="innerHTML">
|
<div id="sidebar-credits" hx-swap-oob="innerHTML">
|
||||||
{{ supplier.credit_balance }} credits
|
{{ supplier.credit_balance }} {{ t.sd_unlocked_credits }}
|
||||||
</div>
|
</div>
|
||||||
{# OOB: update header credits #}
|
{# OOB: update header credits #}
|
||||||
<div id="dl-credit-balance" hx-swap-oob="innerHTML">
|
<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') }}"
|
<a href="{{ url_for('suppliers.dashboard', tab='boosts') }}"
|
||||||
hx-get="{{ url_for('suppliers.dashboard_boosts') }}"
|
hx-get="{{ url_for('suppliers.dashboard_boosts') }}"
|
||||||
hx-target="#dashboard-content"
|
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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<div data-step="1">
|
<div data-step="1">
|
||||||
<h2 class="s-step-title">{% if lang == 'de' %}Plan auswählen{% else %}Choose Your Plan{% endif %}</h2>
|
<h2 class="s-step-title">{{ t.sup_step1_title }}</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>
|
<p class="s-step-sub">{{ t.sup_step1_sub }}</p>
|
||||||
|
|
||||||
<!-- Billing period toggle (CSS sibling selector trick).
|
<!-- Billing period toggle (CSS sibling selector trick).
|
||||||
Radios must be direct siblings of .s-billing-toggle AND <form> so that
|
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 %}>
|
{% if data.get('billing_period', 'yearly') != 'monthly' %}checked{% endif %}>
|
||||||
<div class="s-billing-toggle">
|
<div class="s-billing-toggle">
|
||||||
<div class="s-billing-toggle__pill">
|
<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">
|
<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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -61,19 +61,19 @@
|
|||||||
<label class="s-plan-card {% if data.get('plan', 'supplier_growth') == key %}s-plan-card--selected{% endif %}"
|
<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')">
|
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 %}>
|
<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>
|
<h3>{{ plan.name }}</h3>
|
||||||
<div class="price-yearly">
|
<div class="price-yearly">
|
||||||
<div class="price">€{{ plan.yearly_monthly_equivalent }} <span>/mo</span></div>
|
<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>
|
||||||
<div class="price-monthly">
|
<div class="price-monthly">
|
||||||
<div class="price">€{{ plan.monthly_price }} <span>/mo</span></div>
|
<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>
|
</div>
|
||||||
<ul>
|
<ul>
|
||||||
{% for f in (plan.features_de if lang == 'de' else plan.features) %}
|
{% for key in plan.feature_keys %}
|
||||||
<li>{{ f }}</li>
|
<li>{{ t[key] }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</label>
|
</label>
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
|
|
||||||
<div class="s-nav">
|
<div class="s-nav">
|
||||||
<span></span>
|
<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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<div data-step="2">
|
<div data-step="2">
|
||||||
<h2 class="s-step-title">{% if lang == 'de' %}Boost-Add-ons{% else %}Boost Add-Ons{% endif %}</h2>
|
<h2 class="s-step-title">{{ t.sup_step2_title }}</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>
|
<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) }}"
|
<form hx-post="{{ url_for('suppliers.signup_step', step=2) }}"
|
||||||
hx-target="#signup-step" hx-swap="innerHTML">
|
hx-target="#signup-step" hx-swap="innerHTML">
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<strong>{{ b.name }}</strong>
|
<strong>{{ b.name }}</strong>
|
||||||
{% if is_included %}
|
{% 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 %}
|
{% else %}
|
||||||
<span class="boost-price">€{{ b.price }}/mo</span>
|
<span class="boost-price">€{{ b.price }}/mo</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -33,8 +33,8 @@
|
|||||||
<div class="s-nav">
|
<div class="s-nav">
|
||||||
<button type="button" class="s-btn-back"
|
<button type="button" class="s-btn-back"
|
||||||
hx-get="{{ url_for('suppliers.signup_step', step=1) }}?_accumulated={{ data | tojson | urlencode }}"
|
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>
|
hx-target="#signup-step" hx-swap="innerHTML">{{ t.sup_btn_back }}</button>
|
||||||
<button type="submit" class="s-btn-next">{% if lang == 'de' %}Weiter: Credit-Pakete{% else %}Next: Credit Packs{% endif %}</button>
|
<button type="submit" class="s-btn-next">{{ t.sup_step2_next }}</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<div data-step="3">
|
<div data-step="3">
|
||||||
<h2 class="s-step-title">{% if lang == 'de' %}Credit-Pakete{% else %}Credit Packs{% endif %}</h2>
|
<h2 class="s-step-title">{{ t.sup_step3_title }}</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>
|
<p class="s-step-sub">{{ t.sup_step3_sub }}</p>
|
||||||
|
|
||||||
<form hx-post="{{ url_for('suppliers.signup_step', step=3) }}"
|
<form hx-post="{{ url_for('suppliers.signup_step', step=3) }}"
|
||||||
hx-target="#signup-step" hx-swap="innerHTML">
|
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')">
|
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 %}>
|
<input type="radio" name="credit_pack" value="" {% if not data.get('credit_pack') %}checked{% endif %}>
|
||||||
<div class="amount">0</div>
|
<div class="amount">0</div>
|
||||||
<div class="price">{% if lang == 'de' %}Kostenlos{% else %}Free{% endif %}</div>
|
<div class="price">{{ t.sup_step3_free }}</div>
|
||||||
<div class="per">{% if lang == 'de' %}Nur Plan-Credits{% else %}Plan credits only{% endif %}</div>
|
<div class="per">{{ t.sup_step3_free_desc }}</div>
|
||||||
</label>
|
</label>
|
||||||
{% for cp in credit_packs %}
|
{% for cp in credit_packs %}
|
||||||
<label class="s-credit-card {% if data.get('credit_pack') == cp.key %}s-credit-card--selected{% endif %}"
|
<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"
|
<button type="button" class="s-btn-back"
|
||||||
hx-post="{{ url_for('suppliers.signup_step', step=1) }}"
|
hx-post="{{ url_for('suppliers.signup_step', step=1) }}"
|
||||||
hx-target="#signup-step" hx-swap="innerHTML"
|
hx-target="#signup-step" hx-swap="innerHTML"
|
||||||
hx-include="[name='_accumulated']">{% if lang == 'de' %}Zurück{% else %}Back{% endif %}</button>
|
hx-include="[name='_accumulated']">{{ t.sup_btn_back }}</button>
|
||||||
<button type="submit" class="s-btn-next">{% if lang == 'de' %}Weiter: Deine Daten{% else %}Next: Your Details{% endif %}</button>
|
<button type="submit" class="s-btn-next">{{ t.sup_step3_next }}</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<div data-step="4">
|
<div data-step="4">
|
||||||
<h2 class="s-step-title">{% if lang == 'de' %}Kontodaten{% else %}Account Details{% endif %}</h2>
|
<h2 class="s-step-title">{{ t.sup_step4_title }}</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>
|
<p class="s-step-sub">{{ t.sup_step4_sub }}</p>
|
||||||
|
|
||||||
<form method="post" action="{{ url_for('suppliers.signup_checkout') }}">
|
<form method="post" action="{{ url_for('suppliers.signup_checkout') }}">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<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 style="display:grid;grid-template-columns:1fr 1fr;gap:0 1rem">
|
||||||
<div class="s-field">
|
<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>
|
<input type="text" name="contact_name" class="s-input" value="{{ data.get('contact_name', '') }}" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="s-field">
|
<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>
|
<input type="email" name="contact_email" class="s-input" value="{{ data.get('contact_email', '') }}" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="s-field">
|
<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', '') }}">
|
<input type="tel" name="contact_phone" class="s-input" value="{{ data.get('contact_phone', '') }}">
|
||||||
</div>
|
</div>
|
||||||
<div class="s-field">
|
<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"
|
<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>
|
||||||
<div class="s-field">
|
<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"
|
<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>
|
||||||
<div class="s-field">
|
<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"
|
<input type="text" name="service_area" class="s-input"
|
||||||
value="{{ data.get('service_area', '') }}" placeholder="e.g. DE, ES, FR">
|
value="{{ data.get('service_area', '') }}" placeholder="e.g. DE, ES, FR">
|
||||||
</div>
|
</div>
|
||||||
<div class="s-field">
|
<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"
|
<input type="number" name="years_in_business" class="s-input" min="0"
|
||||||
value="{{ data.get('years_in_business', '') }}">
|
value="{{ data.get('years_in_business', '') }}">
|
||||||
</div>
|
</div>
|
||||||
<div class="s-field">
|
<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"
|
<input type="number" name="project_count" class="s-input" min="0"
|
||||||
value="{{ data.get('project_count', '') }}">
|
value="{{ data.get('project_count', '') }}">
|
||||||
</div>
|
</div>
|
||||||
@@ -48,13 +48,13 @@
|
|||||||
|
|
||||||
{% if data.get('supplier_name') %}
|
{% if data.get('supplier_name') %}
|
||||||
<p style="font-size:12px;color:#64748B;margin-top:0.5rem">
|
<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>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Order Summary -->
|
<!-- Order Summary -->
|
||||||
<div class="s-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">
|
<div class="s-summary-row">
|
||||||
<span>{{ order.plan_name }} Plan</span>
|
<span>{{ order.plan_name }} Plan</span>
|
||||||
<span>
|
<span>
|
||||||
@@ -71,17 +71,17 @@
|
|||||||
</div>
|
</div>
|
||||||
{% if order.boost_monthly > 0 %}
|
{% if order.boost_monthly > 0 %}
|
||||||
<div class="s-summary-row">
|
<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>
|
<span>+€{{ order.boost_monthly }}/mo</span>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="s-summary-row s-summary-total">
|
<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>{% 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' %}{% if lang == 'de' %}/Mon. äquiv.{% else %}/mo equiv.{% endif %}{% else %}/mo{% endif %}</span>
|
<span>€{{ order.monthly_total }}{% if order.billing_period == 'yearly' %}{{ t.sup_step4_equiv }}{% else %}/mo{% endif %}</span>
|
||||||
</div>
|
</div>
|
||||||
{% if order.one_time_total > 0 %}
|
{% if order.one_time_total > 0 %}
|
||||||
<div class="s-summary-row" style="margin-top:8px">
|
<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>
|
<span>€{{ order.one_time_total }}</span>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -91,8 +91,8 @@
|
|||||||
<button type="button" class="s-btn-back"
|
<button type="button" class="s-btn-back"
|
||||||
hx-post="{{ url_for('suppliers.signup_step', step=2) }}"
|
hx-post="{{ url_for('suppliers.signup_step', step=2) }}"
|
||||||
hx-target="#signup-step" hx-swap="innerHTML"
|
hx-target="#signup-step" hx-swap="innerHTML"
|
||||||
hx-include="[name='_accumulated']">{% if lang == 'de' %}Zurück{% else %}Back{% endif %}</button>
|
hx-include="[name='_accumulated']">{{ t.sup_btn_back }}</button>
|
||||||
<button type="submit" class="s-btn-next" id="checkout-btn">{% if lang == 'de' %}Zur Kasse{% else %}Proceed to Checkout{% endif %}</button>
|
<button type="submit" class="s-btn-next" id="checkout-btn">{{ t.sup_step4_checkout }}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="checkout-error" hidden
|
<div id="checkout-error" hidden
|
||||||
@@ -107,7 +107,7 @@
|
|||||||
var btn = document.getElementById('checkout-btn');
|
var btn = document.getElementById('checkout-btn');
|
||||||
var errBox = document.getElementById('checkout-error');
|
var errBox = document.getElementById('checkout-error');
|
||||||
btn.disabled = true;
|
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;
|
errBox.hidden = true;
|
||||||
|
|
||||||
fetch(form.action, {
|
fetch(form.action, {
|
||||||
@@ -118,10 +118,10 @@
|
|||||||
.then(function(res) { return res.json().then(function(d) { return { ok: res.ok, data: d }; }); })
|
.then(function(res) { return res.json().then(function(d) { return { ok: res.ok, data: d }; }); })
|
||||||
.then(function(result) {
|
.then(function(result) {
|
||||||
if (!result.ok || result.data.error) {
|
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;
|
errBox.hidden = false;
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.textContent = '{% if lang == 'de' %}Zur Kasse{% else %}Proceed to Checkout{% endif %}';
|
btn.textContent = {{ t.sup_step4_checkout | tojson }};
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Paddle.Checkout.open({
|
Paddle.Checkout.open({
|
||||||
@@ -130,13 +130,13 @@
|
|||||||
settings: result.data.settings
|
settings: result.data.settings
|
||||||
});
|
});
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.textContent = '{% if lang == 'de' %}Zur Kasse{% else %}Proceed to Checkout{% endif %}';
|
btn.textContent = {{ t.sup_step4_checkout | tojson }};
|
||||||
})
|
})
|
||||||
.catch(function() {
|
.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;
|
errBox.hidden = false;
|
||||||
btn.disabled = 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" %}
|
{% 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 paddle %}{% include "_paddle.html" %}{% endblock %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
@@ -119,7 +119,7 @@
|
|||||||
<div class="s-progress" id="s-progress">
|
<div class="s-progress" id="s-progress">
|
||||||
<div class="s-progress__meta">
|
<div class="s-progress__meta">
|
||||||
<span class="s-progress__label" id="s-progress-label">{{ t.sup_signup_step1 }}</span>
|
<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>
|
||||||
<div class="s-progress__track">
|
<div class="s-progress__track">
|
||||||
<div class="s-progress__fill" id="s-progress-fill" style="width: 25%"></div>
|
<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 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 }}];
|
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-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) + '%';
|
document.getElementById('s-progress-fill').style.width = (n * 25) + '%';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{% extends "base.html" %}
|
{% 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 %}
|
{% block content %}
|
||||||
<main style="background: linear-gradient(180deg, #F1F5F9, #F8FAFC); min-height: 80vh;">
|
<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">
|
<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>
|
<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">
|
<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">✓ {{ t.sup_success_li1 }}</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">✓ {{ t.sup_success_li2 }}</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">✓ {{ t.sup_success_li3 }}</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_li4 }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "base.html" %}
|
{% 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 %}
|
{% block content %}
|
||||||
<main class="container-page py-12">
|
<main class="container-page py-12">
|
||||||
@@ -8,18 +8,17 @@
|
|||||||
{% set plan_info = plans.get(plan, plans['supplier_growth']) %}
|
{% set plan_info = plans.get(plan, plans['supplier_growth']) %}
|
||||||
|
|
||||||
<h1 class="text-2xl mb-1">{{ t.sup_waitlist_h1 }}</h1>
|
<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">
|
<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">
|
<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 key in plan_info.feature_keys[:4] %}
|
||||||
{% for feature in feature_list[:4] %}
|
|
||||||
<li class="flex items-start gap-2">
|
<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">
|
<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>
|
<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>
|
</svg>
|
||||||
<span>{{ feature }}</span>
|
<span>{{ t[key] }}</span>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -40,7 +39,7 @@
|
|||||||
required
|
required
|
||||||
autofocus
|
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>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn w-full">{{ t.sup_waitlist_submit }}</button>
|
<button type="submit" class="btn w-full">{{ t.sup_waitlist_submit }}</button>
|
||||||
|
|||||||
@@ -1,32 +1,32 @@
|
|||||||
{% extends "base.html" %}
|
{% 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 %}
|
{% block content %}
|
||||||
<main class="container-page py-12">
|
<main class="container-page py-12">
|
||||||
<div class="card max-w-sm mx-auto mt-8 text-center">
|
<div class="card max-w-sm mx-auto mt-8 text-center">
|
||||||
{% set plan_info = plans.get(plan, plans['supplier_growth']) %}
|
{% 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="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>
|
<hr>
|
||||||
|
|
||||||
<div class="text-left mt-6">
|
<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">
|
<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>{{ t.sup_waitlist_conf_li1 }}</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>{{ t.sup_waitlist_conf_li2 }}</li>
|
||||||
<li>{% if lang == 'de' %}Vorrangiges Onboarding und Support bei der Eintragsoptimierung{% else %}Priority onboarding and listing optimization support{% endif %}</li>
|
<li>{{ t.sup_waitlist_conf_li3 }}</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_li4 }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="{{ s.lang }}">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<style>{{ css }}</style>
|
<style>{{ css }}</style>
|
||||||
@@ -10,51 +10,47 @@
|
|||||||
<h1>{{ s.title }}</h1>
|
<h1>{{ s.title }}</h1>
|
||||||
<div class="subtitle">{{ s.subtitle }}</div>
|
<div class="subtitle">{{ s.subtitle }}</div>
|
||||||
{% if s.scenario_name %}
|
{% 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 %}
|
{% endif %}
|
||||||
<p class="meta">{{ s.courts }}</p>
|
<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 -->
|
<!-- Executive Summary -->
|
||||||
<h2>{{ s.executive_summary.heading }}</h2>
|
<h2>{{ s.executive_summary.heading }}</h2>
|
||||||
<div class="summary-grid">
|
<div class="summary-grid">
|
||||||
<div class="summary-card">
|
<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 class="value">{{ s.executive_summary.total_capex }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-card">
|
<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 class="value">{{ s.executive_summary.equity }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-card">
|
<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 class="value">{{ s.executive_summary.y3_ebitda }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-card">
|
<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 class="value value--blue">{{ s.executive_summary.irr }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-card">
|
<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 class="value">{{ s.executive_summary.payback }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-card">
|
<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 class="value">{{ s.executive_summary.y1_revenue }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p>This business plan models a <strong>{{ s.executive_summary.facility_type }}</strong> padel facility with
|
<p>{{ s.labels.exec_paragraph }}</p>
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- Investment Plan (CAPEX) -->
|
<!-- Investment Plan (CAPEX) -->
|
||||||
<h2>{{ s.investment.heading }}</h2>
|
<h2>{{ s.investment.heading }}</h2>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<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>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for item in s.investment.items %}
|
{% for item in s.investment.items %}
|
||||||
@@ -65,15 +61,13 @@ of {{ s.executive_summary.payback }}.</p>
|
|||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<tr class="total-row">
|
<tr class="total-row">
|
||||||
<td>Total CAPEX</td>
|
<td>{{ s.labels.total_capex }}</td>
|
||||||
<td style="text-align:right">{{ s.investment.total }}</td>
|
<td style="text-align:right">{{ s.investment.total }}</td>
|
||||||
<td></td>
|
<td></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<p style="font-size:9pt;color:#64748B">
|
<p style="font-size:9pt;color:#64748B">{{ s.labels.capex_stats }}</p>
|
||||||
CAPEX per court: {{ s.investment.per_court }} • CAPEX per m²: {{ s.investment.per_sqm }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- Financing Structure -->
|
<!-- Financing Structure -->
|
||||||
<h2>{{ s.financing.heading }}</h2>
|
<h2>{{ s.financing.heading }}</h2>
|
||||||
@@ -83,13 +77,13 @@ of {{ s.executive_summary.payback }}.</p>
|
|||||||
</div>
|
</div>
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td>Equity</td><td style="text-align:right">{{ s.financing.equity }}</td></tr>
|
<tr><td>{{ s.labels.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>{{ s.labels.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>{{ s.labels.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>{{ s.labels.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>{{ s.labels.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>{{ s.labels.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.ltv }}</td><td style="text-align:right">{{ s.financing.ltv }}</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
@@ -97,7 +91,7 @@ of {{ s.executive_summary.payback }}.</p>
|
|||||||
<h2>{{ s.operations.heading }}</h2>
|
<h2>{{ s.operations.heading }}</h2>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<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>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for item in s.operations.items %}
|
{% for item in s.operations.items %}
|
||||||
@@ -108,24 +102,24 @@ of {{ s.executive_summary.payback }}.</p>
|
|||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<tr class="total-row">
|
<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 style="text-align:right">{{ s.operations.monthly_total }}</td>
|
||||||
<td></td>
|
<td></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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 -->
|
<!-- Revenue Model -->
|
||||||
<h2>{{ s.revenue.heading }}</h2>
|
<h2>{{ s.revenue.heading }}</h2>
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td>Weighted Hourly Rate</td><td style="text-align:right">{{ s.revenue.weighted_rate }}</td></tr>
|
<tr><td>{{ s.labels.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>{{ s.labels.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>{{ s.labels.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>{{ s.labels.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>{{ s.labels.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.monthly_net_cf }}</td><td style="text-align:right">{{ s.revenue.net_cf_monthly }}</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
@@ -133,12 +127,12 @@ of {{ s.executive_summary.payback }}.</p>
|
|||||||
<h2>{{ s.annuals.heading }}</h2>
|
<h2>{{ s.annuals.heading }}</h2>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<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>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for yr in s.annuals.years %}
|
{% for yr in s.annuals.years %}
|
||||||
<tr>
|
<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.revenue }}</td>
|
||||||
<td style="text-align:right">{{ yr.ebitda }}</td>
|
<td style="text-align:right">{{ yr.ebitda }}</td>
|
||||||
<td style="text-align:right">{{ yr.debt_service }}</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>
|
<h2>{{ s.metrics.heading }}</h2>
|
||||||
<div class="metrics-grid">
|
<div class="metrics-grid">
|
||||||
<div class="metric-box">
|
<div class="metric-box">
|
||||||
<div class="label">IRR</div>
|
<div class="label">{{ s.labels.irr }}</div>
|
||||||
<div class="value">{{ s.metrics.irr }}</div>
|
<div class="value">{{ s.metrics.irr }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-box">
|
<div class="metric-box">
|
||||||
<div class="label">MOIC</div>
|
<div class="label">{{ s.labels.moic }}</div>
|
||||||
<div class="value">{{ s.metrics.moic }}</div>
|
<div class="value">{{ s.metrics.moic }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-box">
|
<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 class="value">{{ s.metrics.cash_on_cash }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-box">
|
<div class="metric-box">
|
||||||
<div class="label">Payback</div>
|
<div class="label">{{ s.labels.payback }}</div>
|
||||||
<div class="value">{{ s.metrics.payback }}</div>
|
<div class="value">{{ s.metrics.payback }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-box">
|
<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 class="value">{{ s.metrics.break_even_util }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-box">
|
<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 class="value">{{ s.metrics.ebitda_margin }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-box">
|
<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 class="value">{{ s.metrics.dscr_y3 }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-box">
|
<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 class="value">{{ s.metrics.yield_on_cost }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -189,7 +183,7 @@ of {{ s.executive_summary.payback }}.</p>
|
|||||||
<h2>{{ s.cashflow_12m.heading }}</h2>
|
<h2>{{ s.cashflow_12m.heading }}</h2>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<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>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for m in s.cashflow_12m.months %}
|
{% for m in s.cashflow_12m.months %}
|
||||||
@@ -208,10 +202,7 @@ of {{ s.executive_summary.payback }}.</p>
|
|||||||
|
|
||||||
<!-- Disclaimer -->
|
<!-- Disclaimer -->
|
||||||
<div class="disclaimer">
|
<div class="disclaimer">
|
||||||
<strong>Disclaimer:</strong> This business plan is generated from user-provided assumptions using the Padelnomics
|
{{ s.labels.disclaimer }}
|
||||||
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
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -386,7 +386,7 @@ class TestBakeScenarioCards:
|
|||||||
await _create_published_scenario(slug="cta-test")
|
await _create_published_scenario(slug="cta-test")
|
||||||
html = "[scenario:cta-test]"
|
html = "[scenario:cta-test]"
|
||||||
result = await bake_scenario_cards(html)
|
result = await bake_scenario_cards(html)
|
||||||
assert "/planner/" in result
|
assert "planner" in result
|
||||||
assert "Try with your own numbers" in result
|
assert "Try with your own numbers" in result
|
||||||
|
|
||||||
async def test_invalid_section_ignored(self, db):
|
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