Rewrite frontend templates: Pico CSS → Tailwind + trader-focused copy
Replace all Pico CSS patterns (classless articles, role="button", inline styles, var(--pico-*)) with Tailwind component classes. Add Fraunces display font, mobile hamburger nav, brand chart colors, and new component layer (hero, feature-card, metric-card, auth-card, pricing-card, etc.). Rewrite marketing copy from generic SaaS boilerplate to coffee-trader focused messaging. Rebrand pricing tiers to Explorer/Trader/Analyst. Delete stale custom.css. No Python code changes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,39 +1,36 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Sign In - {{ config.APP_NAME }}{% endblock %}
|
{% block title %}Sign In — {{ config.APP_NAME }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main class="container">
|
<main class="container-page">
|
||||||
<article style="max-width: 400px; margin: 4rem auto;">
|
<div class="auth-card">
|
||||||
<header>
|
<h1>Sign in to BeanFlows</h1>
|
||||||
<h1>Sign In</h1>
|
<p class="subtitle">Enter your email. We'll send a link — no password needed.</p>
|
||||||
<p>Enter your email to receive a sign-in link.</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
|
||||||
<label for="email">
|
<div class="mb-4">
|
||||||
Email
|
<label for="email" class="form-label">Email</label>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
id="email"
|
id="email"
|
||||||
name="email"
|
name="email"
|
||||||
placeholder="you@example.com"
|
class="form-input"
|
||||||
|
placeholder="trader@example.com"
|
||||||
required
|
required
|
||||||
autofocus
|
autofocus
|
||||||
>
|
>
|
||||||
</label>
|
</div>
|
||||||
|
|
||||||
<button type="submit">Send Sign-In Link</button>
|
<button type="submit" class="btn w-full">Send sign-in link</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<footer style="text-align: center; margin-top: 1rem;">
|
<p class="text-center text-sm text-stone mt-6">
|
||||||
<small>
|
|
||||||
Don't have an account?
|
Don't have an account?
|
||||||
<a href="{{ url_for('auth.signup') }}">Sign up</a>
|
<a href="{{ url_for('auth.signup') }}">Sign up</a>
|
||||||
</small>
|
</p>
|
||||||
</footer>
|
</div>
|
||||||
</article>
|
|
||||||
</main>
|
</main>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,35 +1,35 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Check Your Email - {{ config.APP_NAME }}{% endblock %}
|
{% block title %}Check Your Inbox — {{ config.APP_NAME }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main class="container">
|
<main class="container-page">
|
||||||
<article style="max-width: 400px; margin: 4rem auto; text-align: center;">
|
<div class="auth-card text-center">
|
||||||
<header>
|
<div class="text-4xl mb-4">✉</div>
|
||||||
<h1>Check Your Email</h1>
|
<h1>Check your inbox</h1>
|
||||||
</header>
|
<p class="subtitle">We sent a sign-in link to:</p>
|
||||||
|
|
||||||
<p>We've sent a sign-in link to:</p>
|
<p class="font-mono text-espresso bg-latte rounded-lg px-4 py-2 mb-4 text-sm">{{ email }}</p>
|
||||||
<p><strong>{{ email }}</strong></p>
|
|
||||||
|
|
||||||
<p>Click the link in the email to sign in. The link expires in {{ config.MAGIC_LINK_EXPIRY_MINUTES }} minutes.</p>
|
<p class="text-sm text-stone mb-6">Click the link in the email to sign in. It expires in {{ config.MAGIC_LINK_EXPIRY_MINUTES }} minutes.</p>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<details>
|
<details class="faq-item text-left">
|
||||||
<summary>Didn't receive the email?</summary>
|
<summary>Didn't receive the email?</summary>
|
||||||
<ul style="text-align: left;">
|
<div class="pb-4">
|
||||||
|
<ul class="list-disc pl-5 space-y-1 text-sm text-stone mb-4">
|
||||||
<li>Check your spam folder</li>
|
<li>Check your spam folder</li>
|
||||||
<li>Make sure the email address is correct</li>
|
<li>Make sure the email address is correct</li>
|
||||||
<li>Wait a minute and try again</li>
|
<li>Wait a minute and try again</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<form method="post" action="{{ url_for('auth.resend') }}">
|
<form method="post" action="{{ url_for('auth.resend') }}">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<input type="hidden" name="email" value="{{ email }}">
|
<input type="hidden" name="email" value="{{ email }}">
|
||||||
<button type="submit" class="secondary outline">Resend Link</button>
|
<button type="submit" class="btn-outline btn-sm">Resend link</button>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</article>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,44 +1,40 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Sign Up - {{ config.APP_NAME }}{% endblock %}
|
{% block title %}Sign Up — {{ config.APP_NAME }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main class="container">
|
<main class="container-page">
|
||||||
<article style="max-width: 400px; margin: 4rem auto;">
|
<div class="auth-card">
|
||||||
<header>
|
<h1>Create your BeanFlows account</h1>
|
||||||
<h1>Create Account</h1>
|
<p class="subtitle">Free plan. Real data. No credit card.</p>
|
||||||
<p>Enter your email to get started.</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<input type="hidden" name="plan" value="{{ plan }}">
|
<input type="hidden" name="plan" value="{{ plan }}">
|
||||||
|
|
||||||
<label for="email">
|
<div class="mb-4">
|
||||||
Email
|
<label for="email" class="form-label">Email</label>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
id="email"
|
id="email"
|
||||||
name="email"
|
name="email"
|
||||||
placeholder="you@example.com"
|
class="form-input"
|
||||||
|
placeholder="trader@example.com"
|
||||||
required
|
required
|
||||||
autofocus
|
autofocus
|
||||||
>
|
>
|
||||||
</label>
|
|
||||||
|
|
||||||
{% if plan and plan != 'free' %}
|
{% if plan and plan != 'free' %}
|
||||||
<small>You'll be able to subscribe to the <strong>{{ plan | title }}</strong> plan after signing up.</small>
|
<p class="form-hint mt-2">You'll be able to subscribe to the <strong>{{ plan | title }}</strong> plan after signing up.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="submit">Create Account</button>
|
<button type="submit" class="btn w-full">Create account</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<footer style="text-align: center; margin-top: 1rem;">
|
<p class="text-center text-sm text-stone mt-6">
|
||||||
<small>
|
|
||||||
Already have an account?
|
Already have an account?
|
||||||
<a href="{{ url_for('auth.login') }}">Sign in</a>
|
<a href="{{ url_for('auth.login') }}">Sign in</a>
|
||||||
</small>
|
</p>
|
||||||
</footer>
|
</div>
|
||||||
</article>
|
|
||||||
</main>
|
</main>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,119 +1,147 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Pricing - {{ config.APP_NAME }}{% endblock %}
|
{% block title %}Pricing — {{ config.APP_NAME }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main class="container">
|
<main>
|
||||||
<header style="text-align: center; margin-bottom: 3rem;">
|
<!-- Hero -->
|
||||||
<h1>Simple, Transparent Pricing</h1>
|
<section class="hero pb-12">
|
||||||
<p>Start free with coffee data. Upgrade when you need more.</p>
|
<div class="container-page">
|
||||||
</header>
|
<h1 class="heading-display">Pick your depth of data</h1>
|
||||||
|
<p>Every plan includes the full dashboard. Upgrade when you need more history, exports, or API access.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div class="grid">
|
<!-- Pricing Cards -->
|
||||||
<!-- Free Plan -->
|
<section class="container-page pb-16">
|
||||||
<article>
|
<div class="grid-3 items-start">
|
||||||
<header>
|
<!-- Explorer (Free) -->
|
||||||
<h3>Free</h3>
|
<div class="pricing-card">
|
||||||
<p><strong style="font-size: 2rem;">$0</strong> <small>/month</small></p>
|
<div class="mb-6">
|
||||||
</header>
|
<h3 class="text-xl mb-1">Explorer</h3>
|
||||||
<ul>
|
<div class="flex items-baseline gap-1">
|
||||||
<li>Coffee dashboard</li>
|
<span class="text-4xl font-bold text-espresso font-mono">$0</span>
|
||||||
<li>Last 5 years of data</li>
|
<span class="text-stone text-sm">/forever</span>
|
||||||
<li>Global & country charts</li>
|
</div>
|
||||||
<li>Community support</li>
|
</div>
|
||||||
|
<ul class="space-y-2 text-sm text-stone mb-8 flex-1">
|
||||||
|
<li>✓ Full coffee dashboard</li>
|
||||||
|
<li>✓ Last 5 years of data</li>
|
||||||
|
<li>✓ Global & country charts</li>
|
||||||
|
<li>✓ Community support</li>
|
||||||
</ul>
|
</ul>
|
||||||
<footer>
|
<div>
|
||||||
{% if user %}
|
{% if user %}
|
||||||
{% if (user.plan or 'free') == 'free' %}
|
{% if (user.plan or 'free') == 'free' %}
|
||||||
<button class="secondary" disabled>Current Plan</button>
|
<button class="btn-outline w-full" disabled>Current plan</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<button class="secondary" disabled>Free</button>
|
<button class="btn-outline w-full" disabled>Explorer</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{{ url_for('auth.signup', plan='free') }}" role="button" class="secondary">Get Started</a>
|
<a href="{{ url_for('auth.signup', plan='free') }}" class="btn-outline w-full text-center">Start free</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</footer>
|
</div>
|
||||||
</article>
|
</div>
|
||||||
|
|
||||||
<!-- Starter Plan -->
|
<!-- Trader (Starter) — highlighted -->
|
||||||
<article>
|
<div class="pricing-card-highlighted">
|
||||||
<header>
|
<span class="pricing-badge">Most popular</span>
|
||||||
<h3>Starter</h3>
|
<div class="mb-6 pt-2">
|
||||||
<p><strong style="font-size: 2rem;">TBD</strong> <small>/month</small></p>
|
<h3 class="text-xl mb-1">Trader</h3>
|
||||||
</header>
|
<div class="flex items-baseline gap-1">
|
||||||
<ul>
|
<span class="text-4xl font-bold text-espresso font-mono">TBD</span>
|
||||||
<li>Full coffee history (18+ years)</li>
|
<span class="text-stone text-sm">/mo</span>
|
||||||
<li>CSV data export</li>
|
</div>
|
||||||
<li>REST API access (10k calls/mo)</li>
|
</div>
|
||||||
<li>Email support</li>
|
<ul class="space-y-2 text-sm text-stone mb-8 flex-1">
|
||||||
|
<li>✓ Full coffee history (18+ years)</li>
|
||||||
|
<li>✓ CSV data export</li>
|
||||||
|
<li>✓ REST API access (10k calls/mo)</li>
|
||||||
|
<li>✓ Email support</li>
|
||||||
</ul>
|
</ul>
|
||||||
<footer>
|
<div>
|
||||||
{% if user %}
|
{% if user %}
|
||||||
{% if (user.plan or 'free') == 'starter' %}
|
{% if (user.plan or 'free') == 'starter' %}
|
||||||
<button class="secondary" disabled>Current Plan</button>
|
<button class="btn w-full" disabled>Current plan</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<form method="post" action="{{ url_for('billing.checkout', plan='starter') }}">
|
<form method="post" action="{{ url_for('billing.checkout', plan='starter') }}">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<button type="submit">Upgrade</button>
|
<button type="submit" class="btn w-full">Upgrade</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{{ url_for('auth.signup', plan='starter') }}" role="button">Get Started</a>
|
<a href="{{ url_for('auth.signup', plan='starter') }}" class="btn w-full text-center">Get started</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</footer>
|
</div>
|
||||||
</article>
|
</div>
|
||||||
|
|
||||||
<!-- Pro Plan -->
|
<!-- Analyst (Pro) -->
|
||||||
<article>
|
<div class="pricing-card">
|
||||||
<header>
|
<div class="mb-6">
|
||||||
<h3>Pro</h3>
|
<h3 class="text-xl mb-1">Analyst</h3>
|
||||||
<p><strong style="font-size: 2rem;">TBD</strong> <small>/month</small></p>
|
<div class="flex items-baseline gap-1">
|
||||||
</header>
|
<span class="text-4xl font-bold text-espresso font-mono">TBD</span>
|
||||||
<ul>
|
<span class="text-stone text-sm">/mo</span>
|
||||||
<li>All 65 USDA commodities</li>
|
</div>
|
||||||
<li>Unlimited API calls</li>
|
</div>
|
||||||
<li>CSV & API export</li>
|
<ul class="space-y-2 text-sm text-stone mb-8 flex-1">
|
||||||
<li>Priority support</li>
|
<li>✓ All 65 USDA commodities</li>
|
||||||
|
<li>✓ Unlimited API calls</li>
|
||||||
|
<li>✓ CSV & API export</li>
|
||||||
|
<li>✓ Priority support</li>
|
||||||
</ul>
|
</ul>
|
||||||
<footer>
|
<div>
|
||||||
{% if user %}
|
{% if user %}
|
||||||
{% if (user.plan or 'free') == 'pro' %}
|
{% if (user.plan or 'free') == 'pro' %}
|
||||||
<button class="secondary" disabled>Current Plan</button>
|
<button class="btn-outline w-full" disabled>Current plan</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<form method="post" action="{{ url_for('billing.checkout', plan='pro') }}">
|
<form method="post" action="{{ url_for('billing.checkout', plan='pro') }}">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<button type="submit">Upgrade</button>
|
<button type="submit" class="btn-outline w-full">Upgrade</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{{ url_for('auth.signup', plan='pro') }}" role="button">Get Started</a>
|
<a href="{{ url_for('auth.signup', plan='pro') }}" class="btn-outline w-full text-center">Get started</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</footer>
|
|
||||||
</article>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- FAQ -->
|
<!-- FAQ -->
|
||||||
<section style="margin-top: 4rem; max-width: 600px; margin-left: auto; margin-right: auto;">
|
<section class="container-page pb-16 max-w-2xl mx-auto">
|
||||||
<h2>Frequently Asked Questions</h2>
|
<div class="section-heading">
|
||||||
|
<h2>Frequently asked questions</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
<details>
|
<details class="faq-item">
|
||||||
<summary>Where does the data come from?</summary>
|
<summary>Where does the data come from?</summary>
|
||||||
<p>All data comes from the USDA Production, Supply & Distribution (PSD) Online database, which is freely available. We process and transform it daily into analytics-ready metrics.</p>
|
<p>All data comes from the USDA Production, Supply & Distribution (PSD) Online database, which is freely available. We extract, transform, and structure it daily into analytics-ready metrics.</p>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details class="faq-item">
|
||||||
|
<summary>What's the difference between 5 years and 18 years of history?</summary>
|
||||||
|
<p>The free Explorer plan shows the last 5 market years. Trader and Analyst plans unlock the full dataset going back to 2006, giving you deeper context for long-term trend analysis.</p>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details class="faq-item">
|
||||||
<summary>Can I change plans later?</summary>
|
<summary>Can I change plans later?</summary>
|
||||||
<p>Yes. Upgrade or downgrade at any time. Changes take effect immediately with prorated billing.</p>
|
<p>Yes. Upgrade or downgrade at any time. Changes take effect immediately with prorated billing handled by Paddle.</p>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details class="faq-item">
|
||||||
<summary>What commodities are available on Pro?</summary>
|
<summary>What format is the CSV export?</summary>
|
||||||
<p>All 65 commodities tracked by USDA PSD, including coffee, cocoa, sugar, cotton, grains, oilseeds, and more.</p>
|
<p>Standard CSV with headers. One row per country per market year. Compatible with Excel, Google Sheets, Python, R, and any tool that reads CSV.</p>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details class="faq-item">
|
||||||
<summary>How do I cancel?</summary>
|
<summary>How does the API work?</summary>
|
||||||
<p>Cancel anytime from your dashboard settings. You keep access until the end of your billing period.</p>
|
<p>Generate an API key from your dashboard settings. Use Bearer token authentication with standard REST endpoints for commodity listing, time series, and country data. Full docs available after signup.</p>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details class="faq-item">
|
||||||
|
<summary>Is the free plan really free forever?</summary>
|
||||||
|
<p>Yes. No trial period, no credit card required. The Explorer plan is free for as long as you use it.</p>
|
||||||
</details>
|
</details>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,26 +1,27 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Success! - {{ config.APP_NAME }}{% endblock %}
|
{% block title %}You're All Set — {{ config.APP_NAME }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main class="container">
|
<main class="container-page">
|
||||||
<article style="max-width: 500px; margin: 4rem auto; text-align: center;">
|
<div class="auth-card text-center">
|
||||||
<header>
|
<div class="mb-4">
|
||||||
<h1>🎉 Welcome Aboard!</h1>
|
<svg class="mx-auto" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#15803D" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
</header>
|
<circle cx="12" cy="12" r="10"/>
|
||||||
|
<path d="M9 12l2 2 4-4"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h1>You're all set</h1>
|
||||||
|
<p class="subtitle">Your subscription is now active. You have full access to all features included in your plan.</p>
|
||||||
|
|
||||||
<p>Your subscription is now active. You have full access to all features included in your plan.</p>
|
<a href="{{ url_for('dashboard.index') }}" class="btn w-full mb-4">Go to Dashboard</a>
|
||||||
|
|
||||||
<p>
|
|
||||||
<a href="{{ url_for('dashboard.index') }}" role="button">Go to Dashboard</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<p><small>
|
<p class="text-sm text-stone mt-4">
|
||||||
Need to manage your subscription? Visit
|
Need to manage your subscription? Visit
|
||||||
<a href="{{ url_for('dashboard.settings') }}">account settings</a>.
|
<a href="{{ url_for('dashboard.settings') }}">account settings</a>.
|
||||||
</small></p>
|
</p>
|
||||||
</article>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,76 +1,98 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Country Comparison - {{ config.APP_NAME }}{% endblock %}
|
{% block title %}Country Comparison — {{ config.APP_NAME }}{% endblock %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main class="container">
|
<main class="container-page py-8">
|
||||||
<header>
|
<!-- Page Header -->
|
||||||
|
<div class="page-header">
|
||||||
<h1>Country Comparison</h1>
|
<h1>Country Comparison</h1>
|
||||||
<p>Compare coffee metrics across producing and consuming countries.</p>
|
<p>Compare coffee metrics across producing and consuming countries.</p>
|
||||||
</header>
|
</div>
|
||||||
|
|
||||||
<!-- Filters -->
|
<!-- Filters -->
|
||||||
|
<div class="card mb-8">
|
||||||
<form id="country-form" method="get" action="{{ url_for('dashboard.countries') }}">
|
<form id="country-form" method="get" action="{{ url_for('dashboard.countries') }}">
|
||||||
<div class="grid">
|
<div class="grid-2">
|
||||||
<label>
|
<div>
|
||||||
Metric
|
<label for="metric" class="form-label">Metric</label>
|
||||||
<select name="metric" onchange="this.form.submit()">
|
<select name="metric" id="metric" class="form-input" onchange="this.form.submit()">
|
||||||
{% for m in ["Production", "Exports", "Imports", "Ending_Stocks"] %}
|
{% for m in ["Production", "Exports", "Imports", "Ending_Stocks"] %}
|
||||||
<option value="{{ m }}" {{ "selected" if metric == m }}>{{ m | replace("_", " ") }}</option>
|
<option value="{{ m }}" {{ "selected" if metric == m }}>{{ m | replace("_", " ") }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</div>
|
||||||
<label>
|
<div>
|
||||||
Countries (select up to 10)
|
<label for="country" class="form-label">Countries (select up to 10)</label>
|
||||||
<select name="country" multiple size="8" onchange="this.form.submit()">
|
<select name="country" id="country" class="form-input" multiple size="8" onchange="this.form.submit()">
|
||||||
{% for c in all_countries %}
|
{% for c in all_countries %}
|
||||||
<option value="{{ c.country_code }}" {{ "selected" if c.country_code in selected_codes }}>
|
<option value="{{ c.country_code }}" {{ "selected" if c.country_code in selected_codes }}>
|
||||||
{{ c.country_name }}
|
{{ c.country_name }}
|
||||||
</option>
|
</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Chart -->
|
<!-- Chart -->
|
||||||
{% if comparison_data %}
|
{% if comparison_data %}
|
||||||
<section>
|
<div class="chart-container mb-8">
|
||||||
<canvas id="comparisonChart" style="max-height: 500px;"></canvas>
|
<canvas id="comparisonChart"></canvas>
|
||||||
</section>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<article style="text-align: center; color: var(--muted-color);">
|
<div class="plan-gate mb-8">
|
||||||
<p>Select countries above to see the comparison chart.</p>
|
Select countries above to see the comparison chart.
|
||||||
</article>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<a href="{{ url_for('dashboard.index') }}" role="button" class="secondary outline">Back to Dashboard</a>
|
<a href="{{ url_for('dashboard.index') }}" class="btn-outline">Back to Dashboard</a>
|
||||||
</main>
|
</main>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script>
|
<script>
|
||||||
const COLORS = [
|
const CHART_COLORS = {
|
||||||
'#2563eb', '#dc2626', '#16a34a', '#ca8a04', '#9333ea',
|
copper: '#B45309',
|
||||||
'#0891b2', '#e11d48', '#65a30d', '#d97706', '#7c3aed'
|
roast: '#4A2C1A',
|
||||||
|
beanGreen: '#15803D',
|
||||||
|
forest: '#064E3B',
|
||||||
|
stone: '#78716C',
|
||||||
|
espresso: '#2C1810',
|
||||||
|
warning: '#D97706',
|
||||||
|
danger: '#EF4444',
|
||||||
|
parchment: '#E8DFD5',
|
||||||
|
};
|
||||||
|
const CHART_PALETTE = [
|
||||||
|
CHART_COLORS.copper,
|
||||||
|
CHART_COLORS.beanGreen,
|
||||||
|
CHART_COLORS.roast,
|
||||||
|
CHART_COLORS.forest,
|
||||||
|
CHART_COLORS.stone,
|
||||||
|
CHART_COLORS.warning,
|
||||||
|
CHART_COLORS.danger,
|
||||||
|
CHART_COLORS.espresso,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
Chart.defaults.font.family = "'DM Sans', sans-serif";
|
||||||
|
Chart.defaults.color = CHART_COLORS.stone;
|
||||||
|
Chart.defaults.borderColor = CHART_COLORS.parchment;
|
||||||
|
|
||||||
const rawData = {{ comparison_data | tojson }};
|
const rawData = {{ comparison_data | tojson }};
|
||||||
const metric = {{ metric | tojson }};
|
const metric = {{ metric | tojson }};
|
||||||
|
|
||||||
if (rawData.length > 0) {
|
if (rawData.length > 0) {
|
||||||
// Group by country
|
|
||||||
const byCountry = {};
|
const byCountry = {};
|
||||||
for (const row of rawData) {
|
for (const row of rawData) {
|
||||||
if (!byCountry[row.country_name]) byCountry[row.country_name] = [];
|
if (!byCountry[row.country_name]) byCountry[row.country_name] = [];
|
||||||
byCountry[row.country_name].push(row);
|
byCountry[row.country_name].push(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect all years
|
|
||||||
const allYears = [...new Set(rawData.map(r => r.market_year))].sort();
|
const allYears = [...new Set(rawData.map(r => r.market_year))].sort();
|
||||||
|
|
||||||
const datasets = Object.entries(byCountry).map(([name, rows], i) => {
|
const datasets = Object.entries(byCountry).map(([name, rows], i) => {
|
||||||
@@ -78,7 +100,7 @@ if (rawData.length > 0) {
|
|||||||
return {
|
return {
|
||||||
label: name,
|
label: name,
|
||||||
data: allYears.map(y => yearMap[y] ?? null),
|
data: allYears.map(y => yearMap[y] ?? null),
|
||||||
borderColor: COLORS[i % COLORS.length],
|
borderColor: CHART_PALETTE[i % CHART_PALETTE.length],
|
||||||
tension: 0.3,
|
tension: 0.3,
|
||||||
spanGaps: true
|
spanGaps: true
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,100 +1,97 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Dashboard - {{ config.APP_NAME }}{% endblock %}
|
{% block title %}Dashboard — {{ config.APP_NAME }}{% endblock %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main class="container">
|
<main class="container-page py-8">
|
||||||
<header>
|
<!-- Page Header -->
|
||||||
|
<div class="page-header">
|
||||||
<h1>Coffee Dashboard</h1>
|
<h1>Coffee Dashboard</h1>
|
||||||
<p>Welcome back{% if user.name %}, {{ user.name }}{% endif %}! Global coffee market data from USDA PSD.</p>
|
<p>Welcome back{% if user.name %}, {{ user.name }}{% endif %}! Global coffee market data from USDA PSD.</p>
|
||||||
</header>
|
</div>
|
||||||
|
|
||||||
<!-- Key Metric Cards -->
|
<!-- Key Metric Cards -->
|
||||||
<div class="grid">
|
<div class="grid-4 mb-8">
|
||||||
<article>
|
<div class="metric-card">
|
||||||
<header><small>Global Production (latest year)</small></header>
|
<div class="metric-label">Global Production (latest year)</div>
|
||||||
<p style="font-size: 2rem; margin: 0;">
|
<div class="metric-value">{{ "{:,.0f}".format(latest.get("Production", 0)) }}</div>
|
||||||
<strong>{{ "{:,.0f}".format(latest.get("Production", 0)) }}</strong>
|
<div class="metric-sub">1,000 60-kg bags</div>
|
||||||
</p>
|
</div>
|
||||||
<small>1,000 60-kg bags</small>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article>
|
<div class="metric-card">
|
||||||
<header><small>Stock-to-Use Ratio</small></header>
|
<div class="metric-label">Stock-to-Use Ratio</div>
|
||||||
<p style="font-size: 2rem; margin: 0;">
|
<div class="metric-value">
|
||||||
{% if stu_trend %}
|
{% if stu_trend %}
|
||||||
<strong>{{ "{:.1f}".format(stu_trend[-1].get("Stock_to_Use_Ratio_pct", 0)) }}%</strong>
|
{{ "{:.1f}".format(stu_trend[-1].get("Stock_to_Use_Ratio_pct", 0)) }}%
|
||||||
{% else %}
|
{% else %}
|
||||||
<strong>--</strong>
|
--
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</div>
|
||||||
<small>Ending stocks / consumption</small>
|
<div class="metric-sub">Ending stocks / consumption</div>
|
||||||
</article>
|
</div>
|
||||||
|
|
||||||
<article>
|
<div class="metric-card">
|
||||||
<header><small>Trade Balance</small></header>
|
<div class="metric-label">Trade Balance</div>
|
||||||
<p style="font-size: 2rem; margin: 0;">
|
<div class="metric-value">{{ "{:,.0f}".format(latest.get("Exports", 0) - latest.get("Imports", 0)) }}</div>
|
||||||
<strong>{{ "{:,.0f}".format(latest.get("Exports", 0) - latest.get("Imports", 0)) }}</strong>
|
<div class="metric-sub">Exports minus imports</div>
|
||||||
</p>
|
</div>
|
||||||
<small>Exports minus imports</small>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article>
|
<div class="metric-card">
|
||||||
<header><small>Your Plan</small></header>
|
<div class="metric-label">Your Plan</div>
|
||||||
<p style="font-size: 2rem; margin: 0;"><strong>{{ plan | title }}</strong></p>
|
<div class="metric-value">{{ plan | title }}</div>
|
||||||
<small>
|
<div class="metric-sub">
|
||||||
{% if plan == "free" %}
|
{% if plan == "free" %}
|
||||||
<a href="{{ url_for('billing.pricing') }}">Upgrade for full history</a>
|
<a href="{{ url_for('billing.pricing') }}">Upgrade for full history</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ stats.api_calls }} API calls (30d)
|
{{ stats.api_calls }} API calls (30d)
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</small>
|
</div>
|
||||||
</article>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Global Supply/Demand Time Series -->
|
<!-- Global Supply/Demand Time Series -->
|
||||||
<section>
|
<div class="chart-container mb-8">
|
||||||
<h2>Global Supply & Demand</h2>
|
<h2 class="text-xl mb-1">Global Supply & Demand</h2>
|
||||||
{% if plan == "free" %}
|
{% if plan == "free" %}
|
||||||
<p><small>Showing last 5 years. <a href="{{ url_for('billing.pricing') }}">Upgrade</a> for full 18+ year history.</small></p>
|
<div class="plan-gate mb-4">Showing last 5 years. <a href="{{ url_for('billing.pricing') }}">Upgrade</a> for full 18+ year history.</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<canvas id="supplyDemandChart" style="max-height: 400px;"></canvas>
|
<canvas id="supplyDemandChart"></canvas>
|
||||||
</section>
|
</div>
|
||||||
|
|
||||||
<!-- Stock-to-Use Ratio -->
|
<!-- Stock-to-Use Ratio -->
|
||||||
<section>
|
<div class="chart-container mb-8">
|
||||||
<h2>Stock-to-Use Ratio Trend</h2>
|
<h2 class="text-xl mb-4">Stock-to-Use Ratio Trend</h2>
|
||||||
<canvas id="stuChart" style="max-height: 300px;"></canvas>
|
<canvas id="stuChart"></canvas>
|
||||||
</section>
|
</div>
|
||||||
|
|
||||||
<!-- Two-column: Top Producers + YoY Table -->
|
<!-- Two-column: Top Producers + YoY Table -->
|
||||||
<div class="grid">
|
<div class="grid-2 mb-8">
|
||||||
<section>
|
<div class="chart-container">
|
||||||
<h2>Top Producing Countries</h2>
|
<h2 class="text-xl mb-4">Top Producing Countries</h2>
|
||||||
<canvas id="topProducersChart" style="max-height: 400px;"></canvas>
|
<canvas id="topProducersChart"></canvas>
|
||||||
</section>
|
</div>
|
||||||
|
|
||||||
<section>
|
<div class="card">
|
||||||
<h2>Year-over-Year Production Change</h2>
|
<h2 class="text-xl mb-4">Year-over-Year Production Change</h2>
|
||||||
<div style="overflow-x: auto;">
|
<div class="overflow-x-auto">
|
||||||
<table>
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Country</th>
|
<th>Country</th>
|
||||||
<th style="text-align: right;">Production</th>
|
<th class="text-right">Production</th>
|
||||||
<th style="text-align: right;">YoY %</th>
|
<th class="text-right">YoY %</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for row in yoy %}
|
{% for row in yoy %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ row.country_name }}</td>
|
<td>{{ row.country_name }}</td>
|
||||||
<td style="text-align: right;">{{ "{:,.0f}".format(row.Production) }}</td>
|
<td class="text-right">{{ "{:,.0f}".format(row.Production) }}</td>
|
||||||
<td style="text-align: right; color: {{ 'var(--ins-color)' if row.Production_YoY_pct and row.Production_YoY_pct > 0 else 'var(--del-color)' if row.Production_YoY_pct and row.Production_YoY_pct < 0 else 'inherit' }};">
|
<td class="text-right {% if row.Production_YoY_pct and row.Production_YoY_pct > 0 %}text-bean-green{% elif row.Production_YoY_pct and row.Production_YoY_pct < 0 %}text-danger{% endif %}">
|
||||||
{% if row.Production_YoY_pct is not none %}
|
{% if row.Production_YoY_pct is not none %}
|
||||||
{{ "{:+.1f}%".format(row.Production_YoY_pct) }}
|
{{ "{:+.1f}%".format(row.Production_YoY_pct) }}
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -106,39 +103,58 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- CSV Export (plan-gated) -->
|
<!-- CSV Export (plan-gated) -->
|
||||||
{% if plan != "free" %}
|
{% if plan != "free" %}
|
||||||
<section>
|
<div class="mb-8">
|
||||||
<a href="{{ url_for('api.commodity_metrics_csv', code=711100) }}" role="button" class="secondary outline">Export CSV</a>
|
<a href="{{ url_for('api.commodity_metrics_csv', code=711100) }}" class="btn-outline">Export CSV</a>
|
||||||
</section>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<section>
|
<div class="plan-gate mb-8">CSV export available on Trader and Analyst plans. <a href="{{ url_for('billing.pricing') }}">Upgrade</a></div>
|
||||||
<p><small>CSV export available on Starter and Pro plans. <a href="{{ url_for('billing.pricing') }}">Upgrade</a></small></p>
|
|
||||||
</section>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Quick Actions -->
|
<!-- Quick Actions -->
|
||||||
<section>
|
<div class="grid-3">
|
||||||
<div class="grid">
|
<a href="{{ url_for('dashboard.countries') }}" class="btn-outline text-center">Country Comparison</a>
|
||||||
<a href="{{ url_for('dashboard.countries') }}" role="button" class="secondary outline">Country Comparison</a>
|
<a href="{{ url_for('dashboard.settings') }}" class="btn-outline text-center">Settings</a>
|
||||||
<a href="{{ url_for('dashboard.settings') }}" role="button" class="secondary outline">Settings</a>
|
<a href="{{ url_for('dashboard.settings') }}#api-keys" class="btn-outline text-center">API Keys</a>
|
||||||
<a href="{{ url_for('dashboard.settings') }}#api-keys" role="button" class="secondary outline">API Keys</a>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
</main>
|
</main>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script>
|
<script>
|
||||||
// Chart colors
|
// Brand chart colors
|
||||||
const COLORS = [
|
const CHART_COLORS = {
|
||||||
'#2563eb', '#dc2626', '#16a34a', '#ca8a04', '#9333ea',
|
copper: '#B45309',
|
||||||
'#0891b2', '#e11d48', '#65a30d', '#d97706', '#7c3aed'
|
roast: '#4A2C1A',
|
||||||
|
beanGreen: '#15803D',
|
||||||
|
forest: '#064E3B',
|
||||||
|
stone: '#78716C',
|
||||||
|
espresso: '#2C1810',
|
||||||
|
warning: '#D97706',
|
||||||
|
danger: '#EF4444',
|
||||||
|
parchment: '#E8DFD5',
|
||||||
|
latte: '#F5F0EB',
|
||||||
|
};
|
||||||
|
const CHART_PALETTE = [
|
||||||
|
CHART_COLORS.copper,
|
||||||
|
CHART_COLORS.beanGreen,
|
||||||
|
CHART_COLORS.roast,
|
||||||
|
CHART_COLORS.forest,
|
||||||
|
CHART_COLORS.stone,
|
||||||
|
CHART_COLORS.warning,
|
||||||
|
CHART_COLORS.danger,
|
||||||
|
CHART_COLORS.espresso,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Chart.js defaults
|
||||||
|
Chart.defaults.font.family = "'DM Sans', sans-serif";
|
||||||
|
Chart.defaults.color = CHART_COLORS.stone;
|
||||||
|
Chart.defaults.borderColor = CHART_COLORS.parchment;
|
||||||
|
|
||||||
// -- Supply/Demand Chart --
|
// -- Supply/Demand Chart --
|
||||||
const tsData = {{ time_series | tojson }};
|
const tsData = {{ time_series | tojson }};
|
||||||
if (tsData.length > 0) {
|
if (tsData.length > 0) {
|
||||||
@@ -147,11 +163,11 @@ if (tsData.length > 0) {
|
|||||||
data: {
|
data: {
|
||||||
labels: tsData.map(r => r.market_year),
|
labels: tsData.map(r => r.market_year),
|
||||||
datasets: [
|
datasets: [
|
||||||
{label: 'Production', data: tsData.map(r => r.Production), borderColor: COLORS[0], tension: 0.3},
|
{label: 'Production', data: tsData.map(r => r.Production), borderColor: CHART_PALETTE[0], tension: 0.3},
|
||||||
{label: 'Exports', data: tsData.map(r => r.Exports), borderColor: COLORS[1], tension: 0.3},
|
{label: 'Exports', data: tsData.map(r => r.Exports), borderColor: CHART_PALETTE[1], tension: 0.3},
|
||||||
{label: 'Imports', data: tsData.map(r => r.Imports), borderColor: COLORS[2], tension: 0.3},
|
{label: 'Imports', data: tsData.map(r => r.Imports), borderColor: CHART_PALETTE[2], tension: 0.3},
|
||||||
{label: 'Ending Stocks', data: tsData.map(r => r.Ending_Stocks), borderColor: COLORS[3], tension: 0.3},
|
{label: 'Ending Stocks', data: tsData.map(r => r.Ending_Stocks), borderColor: CHART_PALETTE[3], tension: 0.3},
|
||||||
{label: 'Total Distribution', data: tsData.map(r => r.Total_Distribution), borderColor: COLORS[4], tension: 0.3},
|
{label: 'Total Distribution', data: tsData.map(r => r.Total_Distribution), borderColor: CHART_PALETTE[4], tension: 0.3},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
@@ -172,8 +188,8 @@ if (stuData.length > 0) {
|
|||||||
datasets: [{
|
datasets: [{
|
||||||
label: 'Stock-to-Use Ratio (%)',
|
label: 'Stock-to-Use Ratio (%)',
|
||||||
data: stuData.map(r => r.Stock_to_Use_Ratio_pct),
|
data: stuData.map(r => r.Stock_to_Use_Ratio_pct),
|
||||||
borderColor: COLORS[0],
|
borderColor: CHART_COLORS.copper,
|
||||||
backgroundColor: 'rgba(37,99,235,0.1)',
|
backgroundColor: 'rgba(180, 83, 9, 0.08)',
|
||||||
fill: true,
|
fill: true,
|
||||||
tension: 0.3
|
tension: 0.3
|
||||||
}]
|
}]
|
||||||
@@ -196,7 +212,7 @@ if (topData.length > 0) {
|
|||||||
datasets: [{
|
datasets: [{
|
||||||
label: 'Production',
|
label: 'Production',
|
||||||
data: topData.map(r => r.Production),
|
data: topData.map(r => r.Production),
|
||||||
backgroundColor: COLORS[0]
|
backgroundColor: CHART_COLORS.copper
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
|
|||||||
@@ -1,75 +1,75 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Settings - {{ config.APP_NAME }}{% endblock %}
|
{% block title %}Settings — {{ config.APP_NAME }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main class="container">
|
<main class="container-page py-8">
|
||||||
<header>
|
<!-- Page Header -->
|
||||||
|
<div class="page-header">
|
||||||
<h1>Settings</h1>
|
<h1>Settings</h1>
|
||||||
</header>
|
</div>
|
||||||
|
|
||||||
<!-- Profile Section -->
|
<!-- Profile Section -->
|
||||||
<section>
|
<div class="card">
|
||||||
<h2>Profile</h2>
|
<div class="card-header">Profile</div>
|
||||||
<article>
|
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
|
||||||
<label for="email">
|
<div class="mb-4">
|
||||||
Email
|
<label for="email" class="form-label">Email</label>
|
||||||
<input type="email" id="email" value="{{ user.email }}" disabled>
|
<input type="email" id="email" class="form-input bg-latte" value="{{ user.email }}" disabled>
|
||||||
<small>Email cannot be changed</small>
|
<p class="form-hint">Email cannot be changed</p>
|
||||||
</label>
|
</div>
|
||||||
|
|
||||||
<label for="name">
|
<div class="mb-4">
|
||||||
Name
|
<label for="name" class="form-label">Name</label>
|
||||||
<input type="text" id="name" name="name" value="{{ user.name or '' }}" placeholder="Your name">
|
<input type="text" id="name" name="name" class="form-input" value="{{ user.name or '' }}" placeholder="Your name">
|
||||||
</label>
|
</div>
|
||||||
|
|
||||||
<button type="submit">Save Changes</button>
|
<button type="submit" class="btn">Save Changes</button>
|
||||||
</form>
|
</form>
|
||||||
</article>
|
</div>
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Subscription Section -->
|
<!-- Subscription Section -->
|
||||||
<section>
|
<div class="card">
|
||||||
<h2>Subscription</h2>
|
<div class="card-header">Subscription</div>
|
||||||
<article>
|
<div class="grid-3 mb-4">
|
||||||
<div class="grid">
|
|
||||||
<div>
|
<div>
|
||||||
<strong>Current Plan:</strong> {{ (user.plan or 'free') | title }}
|
<span class="text-xs text-stone uppercase tracking-wider">Current Plan</span>
|
||||||
|
<p class="font-semibold text-espresso">{{ (user.plan or 'free') | title }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<strong>Status:</strong> {{ (user.sub_status or 'active') | title }}
|
<span class="text-xs text-stone uppercase tracking-wider">Status</span>
|
||||||
|
<p class="font-semibold text-espresso">{{ (user.sub_status or 'active') | title }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% if user.current_period_end %}
|
{% if user.current_period_end %}
|
||||||
<div>
|
<div>
|
||||||
<strong>Renews:</strong> {{ user.current_period_end[:10] }}
|
<span class="text-xs text-stone uppercase tracking-wider">Renews</span>
|
||||||
|
<p class="font-semibold text-espresso">{{ user.current_period_end[:10] }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-top: 1rem;">
|
<div>
|
||||||
{% if subscription %}
|
{% if subscription %}
|
||||||
<form method="post" action="{{ url_for('billing.portal') }}" style="display: inline;">
|
<form method="post" action="{{ url_for('billing.portal') }}" class="inline">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<button type="submit" class="secondary">Manage Subscription</button>
|
<button type="submit" class="btn-secondary">Manage Subscription</button>
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{{ url_for('billing.pricing') }}" role="button">Upgrade Plan</a>
|
<a href="{{ url_for('billing.pricing') }}" class="btn">Upgrade Plan</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</div>
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- API Keys Section -->
|
<!-- API Keys Section -->
|
||||||
<section id="api-keys">
|
<div class="card" id="api-keys">
|
||||||
<h2>API Keys</h2>
|
<div class="card-header">API Keys</div>
|
||||||
<article>
|
<p class="text-sm text-stone mb-4">API keys allow you to access the API programmatically.</p>
|
||||||
<p>API keys allow you to access the API programmatically.</p>
|
|
||||||
|
|
||||||
{% if api_keys %}
|
{% if api_keys %}
|
||||||
<table>
|
<div class="overflow-x-auto mb-4">
|
||||||
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
@@ -87,69 +87,71 @@
|
|||||||
<td>{{ key.scopes }}</td>
|
<td>{{ key.scopes }}</td>
|
||||||
<td>{{ key.created_at[:10] }}</td>
|
<td>{{ key.created_at[:10] }}</td>
|
||||||
<td>
|
<td>
|
||||||
<form method="post" action="{{ url_for('dashboard.delete_key', key_id=key.id) }}" style="margin: 0;">
|
<form method="post" action="{{ url_for('dashboard.delete_key', key_id=key.id) }}" class="inline">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<button type="submit" class="secondary outline" style="padding: 0.25rem 0.5rem; margin: 0;">Delete</button>
|
<button type="submit" class="btn-danger btn-sm">Delete</button>
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p><em>No API keys yet.</em></p>
|
<p class="text-sm text-stone italic mb-4">No API keys yet.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<details>
|
<details class="faq-item">
|
||||||
<summary>Create New API Key</summary>
|
<summary>Create New API Key</summary>
|
||||||
|
<div class="pt-2 pb-4">
|
||||||
<form method="post" action="{{ url_for('dashboard.create_key') }}">
|
<form method="post" action="{{ url_for('dashboard.create_key') }}">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
|
||||||
<label for="key-name">
|
<div class="mb-4">
|
||||||
Key Name
|
<label for="key-name" class="form-label">Key Name</label>
|
||||||
<input type="text" id="key-name" name="name" placeholder="My API Key" required>
|
<input type="text" id="key-name" name="name" class="form-input" placeholder="My API Key" required>
|
||||||
</label>
|
</div>
|
||||||
|
|
||||||
<fieldset>
|
<div class="mb-4">
|
||||||
<legend>Scopes</legend>
|
<span class="form-label">Scopes</span>
|
||||||
<label>
|
<div class="flex gap-4 mt-1">
|
||||||
<input type="checkbox" name="scopes" value="read" checked>
|
<label class="flex items-center gap-2 text-sm text-stone-dark">
|
||||||
|
<input type="checkbox" name="scopes" value="read" checked class="accent-copper">
|
||||||
Read
|
Read
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label class="flex items-center gap-2 text-sm text-stone-dark">
|
||||||
<input type="checkbox" name="scopes" value="write">
|
<input type="checkbox" name="scopes" value="write" class="accent-copper">
|
||||||
Write
|
Write
|
||||||
</label>
|
</label>
|
||||||
</fieldset>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="submit">Create Key</button>
|
<button type="submit" class="btn">Create Key</button>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</article>
|
</div>
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Danger Zone -->
|
<!-- Danger Zone -->
|
||||||
<section>
|
<div class="danger-card">
|
||||||
<h2>Danger Zone</h2>
|
<h2 class="text-xl text-danger mb-2">Danger Zone</h2>
|
||||||
<article style="border-color: var(--del-color);">
|
<p class="text-sm text-stone mb-4">Once you delete your account, there is no going back. Please be certain.</p>
|
||||||
<p>Once you delete your account, there is no going back. Please be certain.</p>
|
|
||||||
|
|
||||||
<details>
|
<details class="faq-item border-danger/30">
|
||||||
<summary role="button" class="secondary outline" style="--pico-color: var(--del-color);">Delete Account</summary>
|
<summary class="text-danger">Delete Account</summary>
|
||||||
<p>Are you sure? This will:</p>
|
<div class="pt-2 pb-4">
|
||||||
<ul>
|
<p class="text-sm text-stone mb-2">Are you sure? This will:</p>
|
||||||
|
<ul class="list-disc pl-5 space-y-1 text-sm text-stone mb-4">
|
||||||
<li>Delete all your data</li>
|
<li>Delete all your data</li>
|
||||||
<li>Cancel your subscription</li>
|
<li>Cancel your subscription</li>
|
||||||
<li>Remove your API keys</li>
|
<li>Remove your API keys</li>
|
||||||
</ul>
|
</ul>
|
||||||
<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="secondary" style="--pico-background-color: var(--del-color);">
|
<button type="submit" class="btn-danger">Yes, Delete My Account</button>
|
||||||
Yes, Delete My Account
|
|
||||||
</button>
|
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</article>
|
</div>
|
||||||
</section>
|
|
||||||
</main>
|
</main>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,34 +1,48 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}About - {{ config.APP_NAME }}{% endblock %}
|
{% block title %}About — {{ config.APP_NAME }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main class="container">
|
<main>
|
||||||
<article style="max-width: 800px; margin: 0 auto;">
|
<!-- Hero -->
|
||||||
<header style="text-align: center;">
|
<section class="hero">
|
||||||
<h1>About {{ config.APP_NAME }}</h1>
|
<div class="container-page">
|
||||||
</header>
|
<h1 class="heading-display">Coffee data should be accessible to everyone who trades it</h1>
|
||||||
|
<p>BeanFlows exists because the gap between Bloomberg and free USDA flat files shouldn't be $24,000.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section>
|
<!-- Mission -->
|
||||||
<p>{{ config.APP_NAME }} was built with a simple philosophy: ship fast, stay simple.</p>
|
<section class="container-page py-12 max-w-3xl mx-auto">
|
||||||
|
<h2 class="text-2xl mb-4">The problem</h2>
|
||||||
|
<p class="text-stone mb-4">If you're an independent coffee trader, roaster, or analyst, your data options are limited. You either pay for a Bloomberg Terminal you'll use 5% of, spend hours manually downloading and wrangling USDA flat files, or subscribe to a platform built for hedge funds trading 50 commodities at once.</p>
|
||||||
|
<p class="text-stone mb-8">None of these are built for you.</p>
|
||||||
|
|
||||||
<p>Too many SaaS boilerplates are over-engineered. They use PostgreSQL when SQLite would do. They add Redis for a job queue that runs 10 tasks a day. They have 50 npm dependencies for a landing page.</p>
|
<h2 class="text-2xl mb-4">What BeanFlows does</h2>
|
||||||
|
<p class="text-stone mb-4">We take the freely available USDA Production, Supply & Distribution data — the same data the big terminals use — and transform it into clean, analytics-ready metrics. Charts, country comparisons, stock-to-use ratios, CSV exports, and a REST API. Updated daily.</p>
|
||||||
|
<p class="text-stone mb-8">No proprietary black box. The source data is public. Our methodology is transparent.</p>
|
||||||
|
|
||||||
<p>We took a different approach:</p>
|
<h2 class="text-2xl mb-4">How we build it</h2>
|
||||||
|
<p class="text-stone mb-4">Our data pipeline runs daily: extract from USDA, transform through a 4-layer SQL pipeline (raw, staging, cleaned, serving), and deliver to the dashboard. Built with DuckDB, SQLMesh, and Python. No heavy infrastructure, no unnecessary complexity.</p>
|
||||||
|
|
||||||
<ul>
|
<h2 class="text-2xl mb-4">Who this is for</h2>
|
||||||
<li><strong>SQLite for everything</strong> – It handles more than you think.</li>
|
<ul class="space-y-2 text-stone mb-8">
|
||||||
<li><strong>Server-rendered HTML</strong> – No build step, no hydration, no complexity.</li>
|
<li><strong class="text-espresso">Independent traders</strong> who need coffee supply & demand data without a terminal subscription.</li>
|
||||||
<li><strong>Minimal dependencies</strong> – Fewer things to break.</li>
|
<li><strong class="text-espresso">Roasters & importers</strong> tracking origin country production trends for sourcing decisions.</li>
|
||||||
<li><strong>Flat structure</strong> – Find things where you expect them.</li>
|
<li><strong class="text-espresso">Analysts & researchers</strong> who want clean, exportable coffee commodity data.</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p>The result is a codebase you can understand in an afternoon and deploy for $5/month.</p>
|
<h2 class="text-2xl mb-4">The team</h2>
|
||||||
|
<p class="text-stone mb-8">BeanFlows is built and operated by a solo developer who believes commodity data should be simple, affordable, and transparent. No sales team, no enterprise negotiations. Just data.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section style="text-align: center; margin-top: 3rem;">
|
<!-- CTA -->
|
||||||
<a href="{{ url_for('auth.signup') }}" role="button">Get Started</a>
|
<section class="bg-latte py-16">
|
||||||
|
<div class="container-page text-center">
|
||||||
|
<h2 class="heading-display text-3xl md:text-4xl mb-4">See the data for yourself</h2>
|
||||||
|
<p class="text-stone text-lg max-w-xl mx-auto mb-8">Free plan. Real data. No credit card.</p>
|
||||||
|
<a href="{{ url_for('auth.signup') }}" class="btn">Start free</a>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</article>
|
|
||||||
</main>
|
</main>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,73 +1,129 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Features - {{ config.APP_NAME }}{% endblock %}
|
{% block title %}Features — {{ config.APP_NAME }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main class="container">
|
<main>
|
||||||
<header style="text-align: center; margin-bottom: 3rem;">
|
<!-- Hero -->
|
||||||
<h1>Features</h1>
|
<section class="hero">
|
||||||
<p>Coffee market intelligence built on USDA Production, Supply & Distribution data.</p>
|
<div class="container-page">
|
||||||
</header>
|
<h1 class="heading-display">Built for coffee traders, not Wall Street</h1>
|
||||||
|
<p>Every feature exists because an independent trader or roaster needed it.</p>
|
||||||
<section>
|
</div>
|
||||||
<article>
|
|
||||||
<h2>Supply & Demand Dashboard</h2>
|
|
||||||
<p>Interactive charts showing global coffee production, exports, imports, ending stocks, and total distribution by market year. Spot surplus and deficit years at a glance.</p>
|
|
||||||
<ul>
|
|
||||||
<li>18+ years of historical data (2006–present)</li>
|
|
||||||
<li>Line charts for production, trade, and consumption trends</li>
|
|
||||||
<li>Key metric cards for quick orientation</li>
|
|
||||||
<li>Auto-refreshed daily from USDA PSD Online</li>
|
|
||||||
</ul>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article>
|
|
||||||
<h2>Country Analysis & Comparison</h2>
|
|
||||||
<p>Rank the world's coffee producers and consumers. Compare up to 10 countries side-by-side on any metric.</p>
|
|
||||||
<ul>
|
|
||||||
<li>Top-N country rankings (production, exports, imports, stocks)</li>
|
|
||||||
<li>Year-over-year production change table with directional coloring</li>
|
|
||||||
<li>Multi-country overlay charts</li>
|
|
||||||
<li>65 commodity-country combinations from USDA data</li>
|
|
||||||
</ul>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article>
|
|
||||||
<h2>Stock-to-Use Ratio</h2>
|
|
||||||
<p>The ratio traders watch most closely. Track the global coffee stock-to-use ratio over time to gauge market tightness and anticipate price moves.</p>
|
|
||||||
<ul>
|
|
||||||
<li>Global ratio trend chart</li>
|
|
||||||
<li>Ending stocks vs. total distribution breakdown</li>
|
|
||||||
<li>Historical context spanning two decades</li>
|
|
||||||
</ul>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article>
|
|
||||||
<h2>Data Export & API</h2>
|
|
||||||
<p>Download CSV files or integrate directly with your trading systems via REST API.</p>
|
|
||||||
<ul>
|
|
||||||
<li>CSV export of any metric series</li>
|
|
||||||
<li>RESTful JSON API with Bearer token auth</li>
|
|
||||||
<li>Rate-limited and logged for security</li>
|
|
||||||
<li>Commodity listing, time series, and country endpoints</li>
|
|
||||||
</ul>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article>
|
|
||||||
<h2>Daily Data Pipeline</h2>
|
|
||||||
<p>Our pipeline extracts data from the USDA PSD Online database, transforms it through a 4-layer SQL pipeline (raw → staging → cleaned → serving), and delivers analytics-ready metrics every day.</p>
|
|
||||||
<ul>
|
|
||||||
<li>Automated daily extraction from USDA</li>
|
|
||||||
<li>SQLMesh + DuckDB transformation pipeline</li>
|
|
||||||
<li>Incremental processing (only new data each day)</li>
|
|
||||||
<li>Auditable data lineage</li>
|
|
||||||
</ul>
|
|
||||||
</article>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section style="text-align: center; margin-top: 3rem;">
|
<!-- Feature 1: Supply & Demand -->
|
||||||
<a href="{{ url_for('auth.signup') }}" role="button">Start Free</a>
|
<section class="container-page py-12">
|
||||||
<a href="{{ url_for('billing.pricing') }}" role="button" class="secondary outline" style="margin-left: 1rem;">View Pricing</a>
|
<div class="grid-2 items-center">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl md:text-3xl mb-4">Supply & Demand Dashboard</h2>
|
||||||
|
<p class="text-stone mb-4">Interactive charts showing global coffee production, exports, imports, ending stocks, and total distribution by market year. Spot surplus and deficit years at a glance.</p>
|
||||||
|
<ul class="space-y-2 text-sm text-stone">
|
||||||
|
<li>✓ 18+ years of historical data (2006–present)</li>
|
||||||
|
<li>✓ Line charts for production, trade, and consumption trends</li>
|
||||||
|
<li>✓ Key metric cards for quick orientation</li>
|
||||||
|
<li>✓ Auto-refreshed daily from USDA PSD Online</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="bg-latte rounded-2xl h-64 flex items-center justify-center text-stone">
|
||||||
|
<span class="text-sm">Supply & Demand chart preview</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr class="container-page">
|
||||||
|
|
||||||
|
<!-- Feature 2: Country Analysis -->
|
||||||
|
<section class="container-page py-12">
|
||||||
|
<div class="grid-2 items-center">
|
||||||
|
<div class="order-2 md:order-1 bg-latte rounded-2xl h-64 flex items-center justify-center text-stone">
|
||||||
|
<span class="text-sm">Country comparison preview</span>
|
||||||
|
</div>
|
||||||
|
<div class="order-1 md:order-2">
|
||||||
|
<h2 class="text-2xl md:text-3xl mb-4">Country Analysis & Comparison</h2>
|
||||||
|
<p class="text-stone mb-4">Rank the world's coffee producers and consumers. Compare up to 10 countries side-by-side on any metric.</p>
|
||||||
|
<ul class="space-y-2 text-sm text-stone">
|
||||||
|
<li>✓ Top-N country rankings (production, exports, imports, stocks)</li>
|
||||||
|
<li>✓ Year-over-year production change with directional coloring</li>
|
||||||
|
<li>✓ Multi-country overlay charts</li>
|
||||||
|
<li>✓ 65 commodity-country combinations from USDA data</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr class="container-page">
|
||||||
|
|
||||||
|
<!-- Feature 3: Stock-to-Use -->
|
||||||
|
<section class="container-page py-12">
|
||||||
|
<div class="grid-2 items-center">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl md:text-3xl mb-4">Stock-to-Use Ratio</h2>
|
||||||
|
<p class="text-stone mb-4">The ratio traders watch most closely. Track the global coffee stock-to-use ratio over time to gauge market tightness and anticipate price moves.</p>
|
||||||
|
<ul class="space-y-2 text-sm text-stone">
|
||||||
|
<li>✓ Global ratio trend chart spanning two decades</li>
|
||||||
|
<li>✓ Ending stocks vs. total distribution breakdown</li>
|
||||||
|
<li>✓ Historical context for current market conditions</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="bg-latte rounded-2xl h-64 flex items-center justify-center text-stone">
|
||||||
|
<span class="text-sm">Stock-to-use ratio preview</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr class="container-page">
|
||||||
|
|
||||||
|
<!-- Feature 4: Export & API -->
|
||||||
|
<section class="container-page py-12">
|
||||||
|
<div class="grid-2 items-center">
|
||||||
|
<div class="order-2 md:order-1 bg-latte rounded-2xl h-64 flex items-center justify-center text-stone">
|
||||||
|
<span class="text-sm">API response preview</span>
|
||||||
|
</div>
|
||||||
|
<div class="order-1 md:order-2">
|
||||||
|
<h2 class="text-2xl md:text-3xl mb-4">Data Export & API Access</h2>
|
||||||
|
<p class="text-stone mb-4">Download CSV files or integrate directly with your trading systems via REST API.</p>
|
||||||
|
<ul class="space-y-2 text-sm text-stone">
|
||||||
|
<li>✓ CSV export of any metric series</li>
|
||||||
|
<li>✓ RESTful JSON API with Bearer token auth</li>
|
||||||
|
<li>✓ Rate-limited and logged for security</li>
|
||||||
|
<li>✓ Commodity listing, time series, and country endpoints</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr class="container-page">
|
||||||
|
|
||||||
|
<!-- Feature 5: Pipeline -->
|
||||||
|
<section class="container-page py-12">
|
||||||
|
<div class="grid-2 items-center">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl md:text-3xl mb-4">Daily Automated Pipeline</h2>
|
||||||
|
<p class="text-stone mb-4">Our pipeline extracts data from USDA PSD Online, transforms it through a 4-layer SQL pipeline, and delivers analytics-ready metrics every day.</p>
|
||||||
|
<ul class="space-y-2 text-sm text-stone">
|
||||||
|
<li>✓ Automated daily extraction from USDA</li>
|
||||||
|
<li>✓ SQLMesh + DuckDB transformation pipeline</li>
|
||||||
|
<li>✓ Incremental processing (only new data each day)</li>
|
||||||
|
<li>✓ Auditable data lineage</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="bg-latte rounded-2xl h-64 flex items-center justify-center text-stone">
|
||||||
|
<span class="text-sm">Pipeline diagram preview</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- CTA -->
|
||||||
|
<section class="bg-latte py-16">
|
||||||
|
<div class="container-page text-center">
|
||||||
|
<h2 class="heading-display text-3xl md:text-4xl mb-4">Start with the free plan</h2>
|
||||||
|
<p class="text-stone text-lg max-w-xl mx-auto mb-8">Real data, not a demo. 5 years of coffee market data, charts, and metrics.</p>
|
||||||
|
<div class="flex flex-col sm:flex-row gap-3 justify-center">
|
||||||
|
<a href="{{ url_for('auth.signup') }}" class="btn">Start free</a>
|
||||||
|
<a href="{{ url_for('billing.pricing') }}" class="btn-outline">View pricing</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,91 +1,118 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}{{ config.APP_NAME }} - Coffee Market Intelligence for Independent Traders{% endblock %}
|
{% block title %}{{ config.APP_NAME }} — Coffee Market Data Without the Bloomberg Price Tag{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main class="container">
|
<main>
|
||||||
<!-- Hero -->
|
<!-- Hero -->
|
||||||
<header style="text-align: center; padding: 4rem 0;">
|
<section class="hero">
|
||||||
<h1>Coffee Market Intelligence<br>for Independent Traders</h1>
|
<div class="container-page">
|
||||||
<p style="font-size: 1.25rem; max-width: 640px; margin: 0 auto;">
|
<h1 class="heading-display">Coffee market data without the Bloomberg price tag</h1>
|
||||||
Track global supply and demand, compare producing countries, and spot trends
|
<p>18 years of USDA production, trade, and stock data for every coffee-producing country. Charts, exports, and API. Updated daily.</p>
|
||||||
with 18+ years of USDA data. No expensive terminal required.
|
<div class="flex flex-col sm:flex-row gap-3 justify-center">
|
||||||
</p>
|
<a href="{{ url_for('auth.signup') }}" class="btn">Start free — no credit card</a>
|
||||||
<div style="margin-top: 2rem;">
|
<a href="{{ url_for('public.features') }}" class="btn-outline">See what's included</a>
|
||||||
<a href="{{ url_for('auth.signup') }}" role="button" style="margin-right: 1rem;">Start Free</a>
|
|
||||||
<a href="{{ url_for('public.features') }}" role="button" class="secondary outline">See Features</a>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Problem -->
|
||||||
|
<section class="container-page py-16">
|
||||||
|
<div class="section-heading">
|
||||||
|
<h2>Good coffee data shouldn't cost $24,000 a year</h2>
|
||||||
|
<p>Independent traders face bad options for getting the data they need.</p>
|
||||||
|
</div>
|
||||||
|
<div class="grid-3">
|
||||||
|
<div class="feature-card text-center">
|
||||||
|
<div class="feature-icon">💰</div>
|
||||||
|
<h3>Bloomberg Terminal</h3>
|
||||||
|
<p>$24,000/yr — Overkill for most independent traders.</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card text-center">
|
||||||
|
<div class="feature-icon">📄</div>
|
||||||
|
<h3>Manual USDA Digging</h3>
|
||||||
|
<p>$0 but hours of work — No charts, no API, just flat files.</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card text-center">
|
||||||
|
<div class="feature-icon">🏢</div>
|
||||||
|
<h3>Other Platforms</h3>
|
||||||
|
<p>$200–500/mo — Built for hedge funds trading 50 commodities.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Value Props -->
|
<!-- Value Props -->
|
||||||
<section style="padding: 4rem 0;">
|
<section class="bg-latte py-16">
|
||||||
<h2 style="text-align: center;">What You Get</h2>
|
<div class="container-page">
|
||||||
|
<div class="section-heading">
|
||||||
<div class="grid">
|
<h2>What you get</h2>
|
||||||
<article>
|
|
||||||
<h3>Supply & Demand Charts</h3>
|
|
||||||
<p>Global production, exports, imports, ending stocks, and consumption visualized by market year.</p>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article>
|
|
||||||
<h3>Country Analysis</h3>
|
|
||||||
<p>Compare up to 10 producing countries side-by-side. See who's growing, who's shrinking.</p>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article>
|
|
||||||
<h3>Stock-to-Use Ratio</h3>
|
|
||||||
<p>The key indicator traders watch. Track the global ratio over time to gauge tightness.</p>
|
|
||||||
</article>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="grid-3">
|
||||||
<div class="grid">
|
<div class="feature-card">
|
||||||
<article>
|
<div class="feature-icon">📈</div>
|
||||||
|
<h3>Supply & Demand Charts</h3>
|
||||||
|
<p>18+ years of production, exports, imports, and ending stocks. Interactive and filterable by market year.</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">🌎</div>
|
||||||
|
<h3>Country Comparison</h3>
|
||||||
|
<p>Compare up to 10 producing countries side-by-side on any metric. See who's growing, who's shrinking.</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">📊</div>
|
||||||
|
<h3>Stock-to-Use Ratio</h3>
|
||||||
|
<p>The ratio that moves prices. Track the global coffee stock-to-use ratio to gauge market tightness.</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">📥</div>
|
||||||
<h3>CSV & API Export</h3>
|
<h3>CSV & API Export</h3>
|
||||||
<p>Download data for your own models. Integrate with your trading tools via REST API.</p>
|
<p>Your data, your format. Download CSV files or integrate directly with your trading systems via REST API.</p>
|
||||||
</article>
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
<article>
|
<div class="feature-icon">⚡</div>
|
||||||
<h3>Daily Refresh</h3>
|
<h3>Daily Refresh</h3>
|
||||||
<p>Data pipeline runs daily against USDA PSD Online. Always current, always reliable.</p>
|
<p>Our pipeline runs daily against USDA PSD Online. Latest data within hours of USDA publishing.</p>
|
||||||
</article>
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
<article>
|
<div class="feature-icon">🔓</div>
|
||||||
<h3>No Lock-in</h3>
|
<h3>Public Data, Open Method</h3>
|
||||||
<p>Public USDA data, open methodology. You own your exports. Cancel anytime.</p>
|
<p>No black box. Built on freely available USDA data with a fully transparent transformation pipeline.</p>
|
||||||
</article>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- How It Works -->
|
<!-- How It Works -->
|
||||||
<section style="background: var(--card-background-color); border-radius: var(--border-radius); padding: 2rem;">
|
<section class="container-page py-16">
|
||||||
<h2 style="text-align: center;">How It Works</h2>
|
<div class="section-heading">
|
||||||
|
<h2>How it works</h2>
|
||||||
<div class="grid">
|
|
||||||
<div style="text-align: center;">
|
|
||||||
<p style="font-size: 2rem;">1</p>
|
|
||||||
<h4>Sign Up</h4>
|
|
||||||
<p>Enter your email, click the magic link. No password needed.</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="grid-3 text-center">
|
||||||
<div style="text-align: center;">
|
<div>
|
||||||
<p style="font-size: 2rem;">2</p>
|
<div class="step-number">1</div>
|
||||||
<h4>Explore the Dashboard</h4>
|
<h4 class="mb-2">Sign up with your email</h4>
|
||||||
<p>Instant access to coffee supply/demand charts and country rankings.</p>
|
<p class="text-sm text-stone">Magic link, no password. You're in within seconds.</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
<div style="text-align: center;">
|
<div class="step-number">2</div>
|
||||||
<p style="font-size: 2rem;">3</p>
|
<h4 class="mb-2">Explore the dashboard</h4>
|
||||||
<h4>Go Deeper</h4>
|
<p class="text-sm text-stone">Charts and metrics are ready. No setup, no configuration.</p>
|
||||||
<p>Upgrade for full history, CSV exports, and API access for your own models.</p>
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="step-number">3</div>
|
||||||
|
<h4 class="mb-2">Export or connect via API</h4>
|
||||||
|
<p class="text-sm text-stone">CSV downloads or REST API for your own models and tools.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- CTA -->
|
<!-- Final CTA -->
|
||||||
<section style="text-align: center; padding: 4rem 0;">
|
<section class="bg-latte py-16">
|
||||||
<h2>Ready to See the Data?</h2>
|
<div class="container-page text-center">
|
||||||
<p>Free plan includes the last 5 years of coffee market data. No credit card required.</p>
|
<h2 class="heading-display text-3xl md:text-4xl mb-4">See the data before you decide</h2>
|
||||||
<a href="{{ url_for('auth.signup') }}" role="button">Start Free</a>
|
<p class="text-stone text-lg max-w-xl mx-auto mb-8">Free plan includes 5 years of coffee market data. No credit card. No sales call. Just data.</p>
|
||||||
|
<a href="{{ url_for('auth.signup') }}" class="btn">Start free</a>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,35 +1,36 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Privacy Policy - {{ config.APP_NAME }}{% endblock %}
|
{% block title %}Privacy Policy — {{ config.APP_NAME }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main class="container">
|
<main class="container-page py-12">
|
||||||
<article style="max-width: 800px; margin: 0 auto;">
|
<div class="max-w-3xl mx-auto">
|
||||||
<header>
|
<div class="page-header">
|
||||||
<h1>Privacy Policy</h1>
|
<h1>Privacy Policy</h1>
|
||||||
<p><small>Last updated: January 2024</small></p>
|
<p class="text-sm text-stone">Last updated: January 2024</p>
|
||||||
</header>
|
</div>
|
||||||
|
|
||||||
<section>
|
<div class="card">
|
||||||
<h2>1. Information We Collect</h2>
|
<section class="mb-8">
|
||||||
<p>We collect information you provide directly:</p>
|
<h2 class="text-xl mb-3">1. Information We Collect</h2>
|
||||||
<ul>
|
<p class="text-stone mb-2">We collect information you provide directly:</p>
|
||||||
|
<ul class="list-disc pl-5 space-y-1 text-sm text-stone mb-3">
|
||||||
<li>Email address (required for account creation)</li>
|
<li>Email address (required for account creation)</li>
|
||||||
<li>Name (optional)</li>
|
<li>Name (optional)</li>
|
||||||
<li>Payment information (processed by Stripe)</li>
|
<li>Payment information (processed by Paddle)</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>We automatically collect:</p>
|
<p class="text-stone mb-2">We automatically collect:</p>
|
||||||
<ul>
|
<ul class="list-disc pl-5 space-y-1 text-sm text-stone">
|
||||||
<li>IP address</li>
|
<li>IP address</li>
|
||||||
<li>Browser type</li>
|
<li>Browser type</li>
|
||||||
<li>Usage data</li>
|
<li>Usage data</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section class="mb-8">
|
||||||
<h2>2. How We Use Information</h2>
|
<h2 class="text-xl mb-3">2. How We Use Information</h2>
|
||||||
<p>We use your information to:</p>
|
<p class="text-stone mb-2">We use your information to:</p>
|
||||||
<ul>
|
<ul class="list-disc pl-5 space-y-1 text-sm text-stone">
|
||||||
<li>Provide and maintain the service</li>
|
<li>Provide and maintain the service</li>
|
||||||
<li>Process payments</li>
|
<li>Process payments</li>
|
||||||
<li>Send transactional emails</li>
|
<li>Send transactional emails</li>
|
||||||
@@ -38,34 +39,34 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section class="mb-8">
|
||||||
<h2>3. Information Sharing</h2>
|
<h2 class="text-xl mb-3">3. Information Sharing</h2>
|
||||||
<p>We do not sell your personal information. We may share information with:</p>
|
<p class="text-stone mb-2">We do not sell your personal information. We may share information with:</p>
|
||||||
<ul>
|
<ul class="list-disc pl-5 space-y-1 text-sm text-stone">
|
||||||
<li>Service providers (Stripe for payments, Resend for email)</li>
|
<li>Service providers (Paddle for payments, Resend for email)</li>
|
||||||
<li>Law enforcement when required by law</li>
|
<li>Law enforcement when required by law</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section class="mb-8">
|
||||||
<h2>4. Data Retention</h2>
|
<h2 class="text-xl mb-3">4. Data Retention</h2>
|
||||||
<p>We retain your data as long as your account is active. Upon deletion, we remove your data within 30 days.</p>
|
<p class="text-stone">We retain your data as long as your account is active. Upon deletion, we remove your data within 30 days.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section class="mb-8">
|
||||||
<h2>5. Security</h2>
|
<h2 class="text-xl mb-3">5. Security</h2>
|
||||||
<p>We implement industry-standard security measures including encryption, secure sessions, and regular backups.</p>
|
<p class="text-stone">We implement industry-standard security measures including encryption, secure sessions, and regular backups.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section class="mb-8">
|
||||||
<h2>6. Cookies</h2>
|
<h2 class="text-xl mb-3">6. Cookies</h2>
|
||||||
<p>We use essential cookies for session management. We do not use tracking or advertising cookies.</p>
|
<p class="text-stone">We use essential cookies for session management. We do not use tracking or advertising cookies.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section class="mb-8">
|
||||||
<h2>7. Your Rights</h2>
|
<h2 class="text-xl mb-3">7. Your Rights</h2>
|
||||||
<p>You have the right to:</p>
|
<p class="text-stone mb-2">You have the right to:</p>
|
||||||
<ul>
|
<ul class="list-disc pl-5 space-y-1 text-sm text-stone">
|
||||||
<li>Access your data</li>
|
<li>Access your data</li>
|
||||||
<li>Correct inaccurate data</li>
|
<li>Correct inaccurate data</li>
|
||||||
<li>Delete your account and data</li>
|
<li>Delete your account and data</li>
|
||||||
@@ -73,20 +74,21 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section class="mb-8">
|
||||||
<h2>8. GDPR Compliance</h2>
|
<h2 class="text-xl mb-3">8. GDPR Compliance</h2>
|
||||||
<p>For EU users: We process data based on consent and legitimate interest. You may contact us to exercise your GDPR rights.</p>
|
<p class="text-stone">For EU users: We process data based on consent and legitimate interest. You may contact us to exercise your GDPR rights.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="text-xl mb-3">9. Changes</h2>
|
||||||
|
<p class="text-stone">We may update this policy. We will notify you of significant changes via email.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2>9. Changes</h2>
|
<h2 class="text-xl mb-3">10. Contact</h2>
|
||||||
<p>We may update this policy. We will notify you of significant changes via email.</p>
|
<p class="text-stone">For privacy inquiries: {{ config.EMAIL_FROM }}</p>
|
||||||
</section>
|
</section>
|
||||||
|
</div>
|
||||||
<section>
|
</div>
|
||||||
<h2>10. Contact</h2>
|
|
||||||
<p>For privacy inquiries: {{ config.EMAIL_FROM }}</p>
|
|
||||||
</section>
|
|
||||||
</article>
|
|
||||||
</main>
|
</main>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,34 +1,35 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Terms of Service - {{ config.APP_NAME }}{% endblock %}
|
{% block title %}Terms of Service — {{ config.APP_NAME }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main class="container">
|
<main class="container-page py-12">
|
||||||
<article style="max-width: 800px; margin: 0 auto;">
|
<div class="max-w-3xl mx-auto">
|
||||||
<header>
|
<div class="page-header">
|
||||||
<h1>Terms of Service</h1>
|
<h1>Terms of Service</h1>
|
||||||
<p><small>Last updated: January 2024</small></p>
|
<p class="text-sm text-stone">Last updated: January 2024</p>
|
||||||
</header>
|
</div>
|
||||||
|
|
||||||
<section>
|
<div class="card">
|
||||||
<h2>1. Acceptance of Terms</h2>
|
<section class="mb-8">
|
||||||
<p>By accessing or using {{ config.APP_NAME }}, you agree to be bound by these Terms of Service. If you do not agree, do not use the service.</p>
|
<h2 class="text-xl mb-3">1. Acceptance of Terms</h2>
|
||||||
|
<p class="text-stone">By accessing or using {{ config.APP_NAME }}, you agree to be bound by these Terms of Service. If you do not agree, do not use the service.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section class="mb-8">
|
||||||
<h2>2. Description of Service</h2>
|
<h2 class="text-xl mb-3">2. Description of Service</h2>
|
||||||
<p>{{ config.APP_NAME }} provides a software-as-a-service platform. Features and functionality may change over time.</p>
|
<p class="text-stone">{{ config.APP_NAME }} provides a software-as-a-service platform. Features and functionality may change over time.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section class="mb-8">
|
||||||
<h2>3. User Accounts</h2>
|
<h2 class="text-xl mb-3">3. User Accounts</h2>
|
||||||
<p>You are responsible for maintaining the security of your account. You must provide accurate information and keep it updated.</p>
|
<p class="text-stone">You are responsible for maintaining the security of your account. You must provide accurate information and keep it updated.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section class="mb-8">
|
||||||
<h2>4. Acceptable Use</h2>
|
<h2 class="text-xl mb-3">4. Acceptable Use</h2>
|
||||||
<p>You agree not to:</p>
|
<p class="text-stone mb-2">You agree not to:</p>
|
||||||
<ul>
|
<ul class="list-disc pl-5 space-y-1 text-sm text-stone">
|
||||||
<li>Violate any laws or regulations</li>
|
<li>Violate any laws or regulations</li>
|
||||||
<li>Infringe on intellectual property rights</li>
|
<li>Infringe on intellectual property rights</li>
|
||||||
<li>Transmit harmful code or malware</li>
|
<li>Transmit harmful code or malware</li>
|
||||||
@@ -37,35 +38,36 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section class="mb-8">
|
||||||
<h2>5. Payment Terms</h2>
|
<h2 class="text-xl mb-3">5. Payment Terms</h2>
|
||||||
<p>Paid plans are billed in advance. Refunds are handled on a case-by-case basis. We may change pricing with 30 days notice.</p>
|
<p class="text-stone">Paid plans are billed in advance. Refunds are handled on a case-by-case basis. We may change pricing with 30 days notice.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="text-xl mb-3">6. Termination</h2>
|
||||||
|
<p class="text-stone">We may terminate or suspend your account for violations of these terms. You may cancel your account at any time.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="text-xl mb-3">7. Disclaimer of Warranties</h2>
|
||||||
|
<p class="text-stone">The service is provided "as is" without warranties of any kind. We do not guarantee uninterrupted or error-free operation.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="text-xl mb-3">8. Limitation of Liability</h2>
|
||||||
|
<p class="text-stone">We shall not be liable for any indirect, incidental, special, or consequential damages arising from use of the service.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="text-xl mb-3">9. Changes to Terms</h2>
|
||||||
|
<p class="text-stone">We may modify these terms at any time. Continued use after changes constitutes acceptance of the new terms.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2>6. Termination</h2>
|
<h2 class="text-xl mb-3">10. Contact</h2>
|
||||||
<p>We may terminate or suspend your account for violations of these terms. You may cancel your account at any time.</p>
|
<p class="text-stone">For questions about these terms, please contact us at {{ config.EMAIL_FROM }}.</p>
|
||||||
</section>
|
</section>
|
||||||
|
</div>
|
||||||
<section>
|
</div>
|
||||||
<h2>7. Disclaimer of Warranties</h2>
|
|
||||||
<p>The service is provided "as is" without warranties of any kind. We do not guarantee uninterrupted or error-free operation.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<h2>8. Limitation of Liability</h2>
|
|
||||||
<p>We shall not be liable for any indirect, incidental, special, or consequential damages arising from use of the service.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<h2>9. Changes to Terms</h2>
|
|
||||||
<p>We may modify these terms at any time. Continued use after changes constitutes acceptance of the new terms.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<h2>10. Contact</h2>
|
|
||||||
<p>For questions about these terms, please contact us at {{ config.EMAIL_FROM }}.</p>
|
|
||||||
</section>
|
|
||||||
</article>
|
|
||||||
</main>
|
</main>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
/* BeanFlows Custom Styles */
|
|
||||||
|
|
||||||
article {
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
background: var(--code-background-color);
|
|
||||||
padding: 0.125rem 0.25rem;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* HTMX loading indicators */
|
|
||||||
.htmx-indicator {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.htmx-request .htmx-indicator {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.htmx-request.htmx-indicator {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dashboard chart sections */
|
|
||||||
section canvas {
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Key metric cards */
|
|
||||||
article header small {
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
color: var(--muted-color);
|
|
||||||
}
|
|
||||||
442
web/src/beanflows/static/css/input.css
Normal file
442
web/src/beanflows/static/css/input.css
Normal file
@@ -0,0 +1,442 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
/* ── BeanFlows Brand Theme ── */
|
||||||
|
@theme {
|
||||||
|
--font-display: "Fraunces", "DM Sans", ui-serif, Georgia, serif;
|
||||||
|
--font-sans: "DM Sans", ui-sans-serif, system-ui, -apple-system, sans-serif;
|
||||||
|
--font-mono: ui-monospace, "Cascadia Code", monospace;
|
||||||
|
|
||||||
|
--color-espresso: #2C1810;
|
||||||
|
--color-roast: #4A2C1A;
|
||||||
|
--color-copper: #B45309;
|
||||||
|
--color-copper-hover: #92400E;
|
||||||
|
--color-bean-green: #15803D;
|
||||||
|
--color-forest: #064E3B;
|
||||||
|
--color-cream: #FFFBF5;
|
||||||
|
--color-latte: #F5F0EB;
|
||||||
|
--color-parchment: #E8DFD5;
|
||||||
|
--color-stone: #78716C;
|
||||||
|
--color-stone-dark: #57534E;
|
||||||
|
--color-danger: #EF4444;
|
||||||
|
--color-danger-hover: #DC2626;
|
||||||
|
--color-warning: #D97706;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Base layer ── */
|
||||||
|
@layer base {
|
||||||
|
body {
|
||||||
|
@apply bg-cream text-stone-dark font-sans antialiased;
|
||||||
|
}
|
||||||
|
h1, h2, h3 {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
@apply text-espresso font-bold tracking-tight;
|
||||||
|
}
|
||||||
|
h4, h5, h6 {
|
||||||
|
@apply text-roast font-semibold;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
@apply text-copper hover:text-copper-hover transition-colors;
|
||||||
|
}
|
||||||
|
hr {
|
||||||
|
@apply border-parchment my-6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Component classes ── */
|
||||||
|
@layer components {
|
||||||
|
/* ── Navigation ── */
|
||||||
|
.nav-bar {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 50;
|
||||||
|
background: rgba(255, 251, 245, 0.85);
|
||||||
|
backdrop-filter: blur(14px);
|
||||||
|
-webkit-backdrop-filter: blur(14px);
|
||||||
|
border-bottom: 1px solid rgba(232, 223, 213, 0.7);
|
||||||
|
}
|
||||||
|
.nav-inner {
|
||||||
|
max-width: 72rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 56px;
|
||||||
|
}
|
||||||
|
.nav-logo {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
color: var(--color-espresso);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.nav-logo:hover {
|
||||||
|
color: var(--color-espresso);
|
||||||
|
}
|
||||||
|
.nav-links {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.25rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.nav-links a {
|
||||||
|
color: var(--color-stone-dark);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.15s;
|
||||||
|
}
|
||||||
|
.nav-links a:hover {
|
||||||
|
color: var(--color-copper);
|
||||||
|
}
|
||||||
|
a.nav-auth-btn,
|
||||||
|
button.nav-auth-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6px 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
background: var(--color-copper);
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
box-shadow: 0 2px 8px rgba(180, 83, 9, 0.25);
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
a.nav-auth-btn:hover,
|
||||||
|
button.nav-auth-btn:hover {
|
||||||
|
background: var(--color-copper-hover);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.nav-badge {
|
||||||
|
@apply bg-copper/10 text-copper px-2 py-0.5 text-xs font-semibold rounded-full;
|
||||||
|
}
|
||||||
|
.nav-form {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
.nav-hamburger {
|
||||||
|
display: none;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px;
|
||||||
|
color: var(--color-espresso);
|
||||||
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.nav-hamburger { display: block; }
|
||||||
|
.nav-links {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 56px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
background: rgba(255, 251, 245, 0.97);
|
||||||
|
backdrop-filter: blur(14px);
|
||||||
|
-webkit-backdrop-filter: blur(14px);
|
||||||
|
border-bottom: 1px solid rgba(232, 223, 213, 0.7);
|
||||||
|
padding: 1rem;
|
||||||
|
gap: 0.75rem;
|
||||||
|
align-items: stretch;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.nav-links.open { display: flex; }
|
||||||
|
.nav-inner { position: relative; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Page container */
|
||||||
|
.container-page {
|
||||||
|
@apply max-w-6xl mx-auto px-4 sm:px-6 lg:px-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Display headings (Fraunces serif) ── */
|
||||||
|
.heading-display {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
@apply text-espresso font-bold tracking-tight;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Section heading (centered title + subtitle) ── */
|
||||||
|
.section-heading {
|
||||||
|
@apply text-center mb-12;
|
||||||
|
}
|
||||||
|
.section-heading h2 {
|
||||||
|
@apply text-3xl md:text-4xl mb-3;
|
||||||
|
}
|
||||||
|
.section-heading p {
|
||||||
|
@apply text-stone text-lg max-w-2xl mx-auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Hero section ── */
|
||||||
|
.hero {
|
||||||
|
@apply text-center py-20 md:py-28;
|
||||||
|
}
|
||||||
|
.hero h1 {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
@apply text-4xl md:text-5xl lg:text-6xl mb-6 max-w-4xl mx-auto leading-tight;
|
||||||
|
}
|
||||||
|
.hero p {
|
||||||
|
@apply text-lg md:text-xl text-stone max-w-2xl mx-auto mb-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Feature card (icon + heading + text) ── */
|
||||||
|
.feature-card {
|
||||||
|
@apply bg-white border border-parchment rounded-2xl p-6 shadow-sm
|
||||||
|
hover:shadow-md transition-shadow;
|
||||||
|
}
|
||||||
|
.feature-card .feature-icon {
|
||||||
|
@apply text-3xl mb-3;
|
||||||
|
}
|
||||||
|
.feature-card h3 {
|
||||||
|
@apply text-lg mb-2;
|
||||||
|
}
|
||||||
|
.feature-card p {
|
||||||
|
@apply text-sm text-stone leading-relaxed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Metric card (dashboard KPIs) ── */
|
||||||
|
.metric-card {
|
||||||
|
@apply bg-white border border-parchment rounded-2xl p-5 shadow-sm;
|
||||||
|
}
|
||||||
|
.metric-label {
|
||||||
|
@apply text-xs font-semibold text-stone uppercase tracking-wider;
|
||||||
|
}
|
||||||
|
.metric-value {
|
||||||
|
@apply text-2xl md:text-3xl font-bold text-espresso font-mono mt-1;
|
||||||
|
}
|
||||||
|
.metric-sub {
|
||||||
|
@apply text-xs text-stone mt-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Auth card (narrow centered) ── */
|
||||||
|
.auth-card {
|
||||||
|
@apply bg-white border border-parchment rounded-2xl p-8 shadow-sm
|
||||||
|
max-w-md mx-auto my-16;
|
||||||
|
}
|
||||||
|
.auth-card h1 {
|
||||||
|
@apply text-2xl mb-2;
|
||||||
|
}
|
||||||
|
.auth-card p.subtitle {
|
||||||
|
@apply text-stone text-sm mb-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Pricing cards ── */
|
||||||
|
.pricing-card {
|
||||||
|
@apply bg-white border border-parchment rounded-2xl p-6 shadow-sm
|
||||||
|
flex flex-col;
|
||||||
|
}
|
||||||
|
.pricing-card-highlighted {
|
||||||
|
@apply bg-white border-2 border-copper rounded-2xl p-6 shadow-md
|
||||||
|
flex flex-col relative;
|
||||||
|
}
|
||||||
|
.pricing-badge {
|
||||||
|
@apply absolute -top-3 left-1/2 -translate-x-1/2
|
||||||
|
bg-copper text-white text-xs font-semibold px-3 py-1 rounded-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Step number (how-it-works) ── */
|
||||||
|
.step-number {
|
||||||
|
@apply w-10 h-10 rounded-full bg-copper text-white
|
||||||
|
flex items-center justify-center text-sm font-bold mx-auto mb-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── FAQ accordion ── */
|
||||||
|
.faq-item {
|
||||||
|
@apply border-b border-parchment;
|
||||||
|
}
|
||||||
|
.faq-item summary {
|
||||||
|
@apply py-4 cursor-pointer font-medium text-espresso
|
||||||
|
list-none flex justify-between items-center;
|
||||||
|
}
|
||||||
|
.faq-item summary::after {
|
||||||
|
content: "+";
|
||||||
|
@apply text-copper text-xl font-light transition-transform;
|
||||||
|
}
|
||||||
|
.faq-item[open] summary::after {
|
||||||
|
content: "\2212";
|
||||||
|
}
|
||||||
|
.faq-item p {
|
||||||
|
@apply pb-4 text-sm text-stone leading-relaxed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Chart container ── */
|
||||||
|
.chart-container {
|
||||||
|
@apply bg-white border border-parchment rounded-2xl p-4 md:p-6 shadow-sm;
|
||||||
|
}
|
||||||
|
.chart-container canvas {
|
||||||
|
width: 100% !important;
|
||||||
|
max-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Plan gate (upgrade prompt) ── */
|
||||||
|
.plan-gate {
|
||||||
|
@apply bg-latte border border-parchment rounded-xl p-4 text-sm text-stone text-center;
|
||||||
|
}
|
||||||
|
.plan-gate a {
|
||||||
|
@apply font-semibold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Danger card ── */
|
||||||
|
.danger-card {
|
||||||
|
@apply bg-white border-2 border-danger/30 rounded-2xl p-6 shadow-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Page header (dashboard) ── */
|
||||||
|
.page-header {
|
||||||
|
@apply mb-8;
|
||||||
|
}
|
||||||
|
.page-header h1 {
|
||||||
|
@apply text-3xl mb-1;
|
||||||
|
}
|
||||||
|
.page-header p {
|
||||||
|
@apply text-stone text-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cards */
|
||||||
|
.card {
|
||||||
|
@apply bg-white border border-parchment rounded-2xl p-6 mb-6 shadow-sm;
|
||||||
|
}
|
||||||
|
.card-header {
|
||||||
|
@apply border-b border-parchment pb-3 mb-4 text-sm text-stone font-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons — shared base */
|
||||||
|
.btn, .btn-secondary, .btn-danger {
|
||||||
|
@apply inline-flex items-center justify-center px-5 py-2.5 rounded-xl
|
||||||
|
font-semibold text-sm transition-colors cursor-pointer
|
||||||
|
focus:outline-none focus:ring-2 focus:ring-copper/50;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
@apply bg-copper text-white hover:bg-copper-hover shadow-[0_2px_10px_rgba(180,83,9,0.25)];
|
||||||
|
}
|
||||||
|
.btn-secondary {
|
||||||
|
@apply bg-stone-dark text-white hover:bg-espresso;
|
||||||
|
}
|
||||||
|
.btn-danger {
|
||||||
|
@apply bg-danger text-white hover:bg-danger-hover;
|
||||||
|
}
|
||||||
|
.btn-outline {
|
||||||
|
@apply inline-flex items-center justify-center px-5 py-2.5 rounded-xl
|
||||||
|
font-semibold text-sm transition-colors cursor-pointer
|
||||||
|
bg-transparent text-stone-dark border border-parchment
|
||||||
|
hover:bg-latte hover:text-espresso
|
||||||
|
focus:outline-none focus:ring-2 focus:ring-copper/50;
|
||||||
|
}
|
||||||
|
.btn-sm {
|
||||||
|
@apply px-3 py-1.5 text-xs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Forms */
|
||||||
|
.form-label {
|
||||||
|
@apply block text-sm font-medium text-roast mb-1;
|
||||||
|
}
|
||||||
|
.form-input {
|
||||||
|
@apply w-full px-3 py-2 rounded-xl border border-parchment bg-white
|
||||||
|
text-stone-dark placeholder-stone
|
||||||
|
focus:outline-none focus:ring-2 focus:ring-copper/50 focus:border-copper
|
||||||
|
transition-colors;
|
||||||
|
}
|
||||||
|
.form-hint {
|
||||||
|
@apply text-xs text-stone mt-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tables */
|
||||||
|
.table {
|
||||||
|
@apply w-full text-sm;
|
||||||
|
}
|
||||||
|
.table th {
|
||||||
|
@apply text-left px-3 py-2 text-xs font-semibold text-stone uppercase tracking-wider
|
||||||
|
border-b-2 border-parchment;
|
||||||
|
}
|
||||||
|
.table td {
|
||||||
|
@apply px-3 py-2 border-b border-parchment text-stone-dark;
|
||||||
|
}
|
||||||
|
.table tbody tr:hover {
|
||||||
|
@apply bg-latte;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Flash messages */
|
||||||
|
.flash, .flash-error, .flash-success, .flash-warning {
|
||||||
|
@apply px-4 py-3 rounded-xl mb-4 border-l-4 bg-white text-stone-dark text-sm;
|
||||||
|
}
|
||||||
|
.flash {
|
||||||
|
@apply border-copper;
|
||||||
|
}
|
||||||
|
.flash-error {
|
||||||
|
@apply border-danger;
|
||||||
|
}
|
||||||
|
.flash-success {
|
||||||
|
@apply border-bean-green;
|
||||||
|
}
|
||||||
|
.flash-warning {
|
||||||
|
@apply border-warning;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badges */
|
||||||
|
.badge, .badge-success, .badge-danger, .badge-warning {
|
||||||
|
@apply inline-block px-2 py-0.5 text-xs font-semibold rounded-full;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
@apply bg-copper/10 text-copper;
|
||||||
|
}
|
||||||
|
.badge-success {
|
||||||
|
@apply bg-bean-green/10 text-bean-green;
|
||||||
|
}
|
||||||
|
.badge-danger {
|
||||||
|
@apply bg-danger/10 text-danger;
|
||||||
|
}
|
||||||
|
.badge-warning {
|
||||||
|
@apply bg-warning/10 text-warning;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Heading group */
|
||||||
|
.heading-group {
|
||||||
|
@apply mb-6;
|
||||||
|
}
|
||||||
|
.heading-group h1,
|
||||||
|
.heading-group h2 {
|
||||||
|
@apply mb-1;
|
||||||
|
}
|
||||||
|
.heading-group p {
|
||||||
|
@apply text-stone text-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grid helpers */
|
||||||
|
.grid-auto {
|
||||||
|
@apply grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6;
|
||||||
|
}
|
||||||
|
.grid-2 {
|
||||||
|
@apply grid grid-cols-1 md:grid-cols-2 gap-6;
|
||||||
|
}
|
||||||
|
.grid-3 {
|
||||||
|
@apply grid grid-cols-1 md:grid-cols-3 gap-6;
|
||||||
|
}
|
||||||
|
.grid-4 {
|
||||||
|
@apply grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Monospace data display */
|
||||||
|
.metric {
|
||||||
|
@apply font-mono text-espresso;
|
||||||
|
}
|
||||||
|
.mono {
|
||||||
|
@apply font-mono;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* HTMX loading indicators */
|
||||||
|
.htmx-indicator {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.htmx-request .htmx-indicator,
|
||||||
|
.htmx-request.htmx-indicator {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code blocks */
|
||||||
|
code {
|
||||||
|
@apply font-mono text-sm bg-latte px-1 py-0.5 rounded;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,59 +1,60 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" data-theme="light">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{% block title %}{{ config.APP_NAME }}{% endblock %}</title>
|
<title>{% block title %}{{ config.APP_NAME }}{% endblock %}</title>
|
||||||
|
|
||||||
<!-- Pico CSS -->
|
<!-- Fonts -->
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&family=Fraunces:ital,opsz,wght@0,9..144,100..900;1,9..144,100..900&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
<!-- Custom styles -->
|
<!-- Tailwind CSS -->
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/custom.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/output.css') }}">
|
||||||
|
|
||||||
{% block head %}{% endblock %}
|
{% block head %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Navigation -->
|
<!-- Navigation -->
|
||||||
<nav class="container">
|
<nav class="nav-bar">
|
||||||
<ul>
|
<div class="nav-inner">
|
||||||
<li><a href="{{ url_for('public.landing') }}"><strong>{{ config.APP_NAME }}</strong></a></li>
|
<a href="{{ url_for('public.landing') }}" class="nav-logo">{{ config.APP_NAME }}</a>
|
||||||
</ul>
|
<button class="nav-hamburger" aria-label="Toggle menu" onclick="document.querySelector('.nav-links').classList.toggle('open')">
|
||||||
<ul>
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||||
<li><a href="{{ url_for('public.features') }}">Features</a></li>
|
<line x1="3" y1="6" x2="21" y2="6"/>
|
||||||
<li><a href="{{ url_for('billing.pricing') }}">Pricing</a></li>
|
<line x1="3" y1="12" x2="21" y2="12"/>
|
||||||
|
<line x1="3" y1="18" x2="21" y2="18"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div class="nav-links">
|
||||||
|
<a href="{{ url_for('public.features') }}">Features</a>
|
||||||
|
<a href="{{ url_for('billing.pricing') }}">Pricing</a>
|
||||||
{% if user %}
|
{% if user %}
|
||||||
<li><a href="{{ url_for('dashboard.index') }}">Dashboard</a></li>
|
<a href="{{ url_for('dashboard.index') }}">Dashboard</a>
|
||||||
{% if session.get('is_admin') %}
|
{% if session.get('is_admin') %}
|
||||||
<li><a href="{{ url_for('admin.index') }}"><mark>Admin</mark></a></li>
|
<a href="{{ url_for('admin.index') }}"><span class="nav-badge">Admin</span></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li>
|
<form method="post" action="{{ url_for('auth.logout') }}" class="nav-form">
|
||||||
<form method="post" action="{{ url_for('auth.logout') }}" style="margin: 0;">
|
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<button type="submit" class="outline secondary" style="padding: 0.5rem 1rem; margin: 0;">Sign Out</button>
|
<button type="submit" class="btn-outline btn-sm">Sign Out</button>
|
||||||
</form>
|
</form>
|
||||||
</li>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<li><a href="{{ url_for('auth.login') }}">Sign In</a></li>
|
<a href="{{ url_for('auth.login') }}">Sign In</a>
|
||||||
<li><a href="{{ url_for('auth.signup') }}" role="button">Get Started</a></li>
|
<a href="{{ url_for('auth.signup') }}" class="nav-auth-btn">Get Started</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</div>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- Flash messages -->
|
<!-- Flash messages -->
|
||||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
<div class="container">
|
<div class="container-page mt-4">
|
||||||
{% for category, message in messages %}
|
{% for category, message in messages %}
|
||||||
<article
|
<div class="{% if category == 'error' %}flash-error{% elif category == 'success' %}flash-success{% elif category == 'warning' %}flash-warning{% else %}flash{% endif %}">
|
||||||
style="padding: 1rem; margin-bottom: 1rem;
|
|
||||||
{% if category == 'error' %}border-left: 4px solid var(--del-color);
|
|
||||||
{% elif category == 'success' %}border-left: 4px solid var(--ins-color);
|
|
||||||
{% elif category == 'warning' %}border-left: 4px solid var(--mark-background-color);
|
|
||||||
{% else %}border-left: 4px solid var(--primary);{% endif %}"
|
|
||||||
>
|
|
||||||
{{ message }}
|
{{ message }}
|
||||||
</article>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -62,34 +63,36 @@
|
|||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<footer class="container" style="margin-top: 4rem; padding: 2rem 0; border-top: 1px solid var(--muted-border-color);">
|
<footer class="bg-latte mt-20">
|
||||||
<div class="grid">
|
<div class="container-page py-12">
|
||||||
|
<div class="grid-3">
|
||||||
<div>
|
<div>
|
||||||
<strong>{{ config.APP_NAME }}</strong>
|
<strong class="heading-display text-lg">{{ config.APP_NAME }}</strong>
|
||||||
<p><small>Coffee market intelligence for independent traders.</small></p>
|
<p class="text-sm text-stone mt-2 max-w-xs">Coffee commodity data for traders who refuse to overpay for market intelligence.</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<strong>Product</strong>
|
<strong class="text-espresso text-sm">Product</strong>
|
||||||
<ul style="list-style: none; padding: 0;">
|
<ul class="list-none p-0 mt-2 space-y-1.5 text-sm">
|
||||||
<li><a href="{{ url_for('public.features') }}">Features</a></li>
|
<li><a href="{{ url_for('public.features') }}">Features</a></li>
|
||||||
<li><a href="{{ url_for('billing.pricing') }}">Pricing</a></li>
|
<li><a href="{{ url_for('billing.pricing') }}">Pricing</a></li>
|
||||||
<li><a href="{{ url_for('public.about') }}">About</a></li>
|
<li><a href="{{ url_for('public.about') }}">About</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<strong>Legal</strong>
|
<strong class="text-espresso text-sm">Legal</strong>
|
||||||
<ul style="list-style: none; padding: 0;">
|
<ul class="list-none p-0 mt-2 space-y-1.5 text-sm">
|
||||||
<li><a href="{{ url_for('public.terms') }}">Terms</a></li>
|
<li><a href="{{ url_for('public.terms') }}">Terms</a></li>
|
||||||
<li><a href="{{ url_for('public.privacy') }}">Privacy</a></li>
|
<li><a href="{{ url_for('public.privacy') }}">Privacy</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p style="text-align: center; margin-top: 2rem;">
|
<div class="border-t border-parchment mt-8 pt-6 text-center text-xs text-stone">
|
||||||
<small>© {{ now.year }} {{ config.APP_NAME }}. All rights reserved.</small>
|
© {{ now.year }} {{ config.APP_NAME }}. All rights reserved.
|
||||||
</p>
|
</div>
|
||||||
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<!-- HTMX (optional) -->
|
<!-- HTMX -->
|
||||||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||||
|
|
||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user