Merge branch 'frontend-upgrade'
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,39 +1,36 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Sign In - {{ config.APP_NAME }}{% endblock %}
|
||||
{% block title %}Sign In — {{ config.APP_NAME }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="container">
|
||||
<article style="max-width: 400px; margin: 4rem auto;">
|
||||
<header>
|
||||
<h1>Sign In</h1>
|
||||
<p>Enter your email to receive a sign-in link.</p>
|
||||
</header>
|
||||
<main class="container-page">
|
||||
<div class="auth-card">
|
||||
<h1>Sign in to BeanFlows</h1>
|
||||
<p class="subtitle">Enter your email. We'll send a link — no password needed.</p>
|
||||
|
||||
<form method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<label for="email">
|
||||
Email
|
||||
<div class="mb-4">
|
||||
<label for="email" class="form-label">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
placeholder="you@example.com"
|
||||
class="form-input"
|
||||
placeholder="trader@example.com"
|
||||
required
|
||||
autofocus
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit">Send Sign-In Link</button>
|
||||
<button type="submit" class="btn w-full">Send sign-in link</button>
|
||||
</form>
|
||||
|
||||
<footer style="text-align: center; margin-top: 1rem;">
|
||||
<small>
|
||||
<p class="text-center text-sm text-stone mt-6">
|
||||
Don't have an account?
|
||||
<a href="{{ url_for('auth.signup') }}">Sign up</a>
|
||||
</small>
|
||||
</footer>
|
||||
</article>
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,35 +1,35 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Check Your Email - {{ config.APP_NAME }}{% endblock %}
|
||||
{% block title %}Check Your Inbox — {{ config.APP_NAME }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="container">
|
||||
<article style="max-width: 400px; margin: 4rem auto; text-align: center;">
|
||||
<header>
|
||||
<h1>Check Your Email</h1>
|
||||
</header>
|
||||
<main class="container-page">
|
||||
<div class="auth-card text-center">
|
||||
<div class="text-4xl mb-4">✉</div>
|
||||
<h1>Check your inbox</h1>
|
||||
<p class="subtitle">We sent a sign-in link to:</p>
|
||||
|
||||
<p>We've sent a sign-in link to:</p>
|
||||
<p><strong>{{ email }}</strong></p>
|
||||
<p class="font-mono text-espresso bg-latte rounded-lg px-4 py-2 mb-4 text-sm">{{ email }}</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>
|
||||
|
||||
<details>
|
||||
<details class="faq-item text-left">
|
||||
<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>Make sure the email address is correct</li>
|
||||
<li>Wait a minute and try again</li>
|
||||
</ul>
|
||||
|
||||
<form method="post" action="{{ url_for('auth.resend') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<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>
|
||||
</div>
|
||||
</details>
|
||||
</article>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,44 +1,40 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Sign Up - {{ config.APP_NAME }}{% endblock %}
|
||||
{% block title %}Sign Up — {{ config.APP_NAME }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="container">
|
||||
<article style="max-width: 400px; margin: 4rem auto;">
|
||||
<header>
|
||||
<h1>Create Account</h1>
|
||||
<p>Enter your email to get started.</p>
|
||||
</header>
|
||||
<main class="container-page">
|
||||
<div class="auth-card">
|
||||
<h1>Create your BeanFlows account</h1>
|
||||
<p class="subtitle">Free plan. Real data. No credit card.</p>
|
||||
|
||||
<form method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="plan" value="{{ plan }}">
|
||||
|
||||
<label for="email">
|
||||
Email
|
||||
<div class="mb-4">
|
||||
<label for="email" class="form-label">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
placeholder="you@example.com"
|
||||
class="form-input"
|
||||
placeholder="trader@example.com"
|
||||
required
|
||||
autofocus
|
||||
>
|
||||
</label>
|
||||
|
||||
{% 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 %}
|
||||
</div>
|
||||
|
||||
<button type="submit">Create Account</button>
|
||||
<button type="submit" class="btn w-full">Create account</button>
|
||||
</form>
|
||||
|
||||
<footer style="text-align: center; margin-top: 1rem;">
|
||||
<small>
|
||||
<p class="text-center text-sm text-stone mt-6">
|
||||
Already have an account?
|
||||
<a href="{{ url_for('auth.login') }}">Sign in</a>
|
||||
</small>
|
||||
</footer>
|
||||
</article>
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,119 +1,147 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Pricing - {{ config.APP_NAME }}{% endblock %}
|
||||
{% block title %}Pricing — {{ config.APP_NAME }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="container">
|
||||
<header style="text-align: center; margin-bottom: 3rem;">
|
||||
<h1>Simple, Transparent Pricing</h1>
|
||||
<p>Start free with coffee data. Upgrade when you need more.</p>
|
||||
</header>
|
||||
<main>
|
||||
<!-- Hero -->
|
||||
<section class="hero pb-12">
|
||||
<div class="container-page">
|
||||
<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">
|
||||
<!-- Free Plan -->
|
||||
<article>
|
||||
<header>
|
||||
<h3>Free</h3>
|
||||
<p><strong style="font-size: 2rem;">$0</strong> <small>/month</small></p>
|
||||
</header>
|
||||
<ul>
|
||||
<li>Coffee dashboard</li>
|
||||
<li>Last 5 years of data</li>
|
||||
<li>Global & country charts</li>
|
||||
<li>Community support</li>
|
||||
<!-- Pricing Cards -->
|
||||
<section class="container-page pb-16">
|
||||
<div class="grid-3 items-start">
|
||||
<!-- Explorer (Free) -->
|
||||
<div class="pricing-card">
|
||||
<div class="mb-6">
|
||||
<h3 class="text-xl mb-1">Explorer</h3>
|
||||
<div class="flex items-baseline gap-1">
|
||||
<span class="text-4xl font-bold text-espresso font-mono">$0</span>
|
||||
<span class="text-stone text-sm">/forever</span>
|
||||
</div>
|
||||
</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>
|
||||
<footer>
|
||||
<div>
|
||||
{% if user %}
|
||||
{% if (user.plan or 'free') == 'free' %}
|
||||
<button class="secondary" disabled>Current Plan</button>
|
||||
<button class="btn-outline w-full" disabled>Current plan</button>
|
||||
{% else %}
|
||||
<button class="secondary" disabled>Free</button>
|
||||
<button class="btn-outline w-full" disabled>Explorer</button>
|
||||
{% endif %}
|
||||
{% 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 %}
|
||||
</footer>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Starter Plan -->
|
||||
<article>
|
||||
<header>
|
||||
<h3>Starter</h3>
|
||||
<p><strong style="font-size: 2rem;">TBD</strong> <small>/month</small></p>
|
||||
</header>
|
||||
<ul>
|
||||
<li>Full coffee history (18+ years)</li>
|
||||
<li>CSV data export</li>
|
||||
<li>REST API access (10k calls/mo)</li>
|
||||
<li>Email support</li>
|
||||
<!-- Trader (Starter) — highlighted -->
|
||||
<div class="pricing-card-highlighted">
|
||||
<span class="pricing-badge">Most popular</span>
|
||||
<div class="mb-6 pt-2">
|
||||
<h3 class="text-xl mb-1">Trader</h3>
|
||||
<div class="flex items-baseline gap-1">
|
||||
<span class="text-4xl font-bold text-espresso font-mono">TBD</span>
|
||||
<span class="text-stone text-sm">/mo</span>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<footer>
|
||||
<div>
|
||||
{% if user %}
|
||||
{% if (user.plan or 'free') == 'starter' %}
|
||||
<button class="secondary" disabled>Current Plan</button>
|
||||
<button class="btn w-full" disabled>Current plan</button>
|
||||
{% else %}
|
||||
<form method="post" action="{{ url_for('billing.checkout', plan='starter') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit">Upgrade</button>
|
||||
<button type="submit" class="btn w-full">Upgrade</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% 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 %}
|
||||
</footer>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pro Plan -->
|
||||
<article>
|
||||
<header>
|
||||
<h3>Pro</h3>
|
||||
<p><strong style="font-size: 2rem;">TBD</strong> <small>/month</small></p>
|
||||
</header>
|
||||
<ul>
|
||||
<li>All 65 USDA commodities</li>
|
||||
<li>Unlimited API calls</li>
|
||||
<li>CSV & API export</li>
|
||||
<li>Priority support</li>
|
||||
<!-- Analyst (Pro) -->
|
||||
<div class="pricing-card">
|
||||
<div class="mb-6">
|
||||
<h3 class="text-xl mb-1">Analyst</h3>
|
||||
<div class="flex items-baseline gap-1">
|
||||
<span class="text-4xl font-bold text-espresso font-mono">TBD</span>
|
||||
<span class="text-stone text-sm">/mo</span>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="space-y-2 text-sm text-stone mb-8 flex-1">
|
||||
<li>✓ All 65 USDA commodities</li>
|
||||
<li>✓ Unlimited API calls</li>
|
||||
<li>✓ CSV & API export</li>
|
||||
<li>✓ Priority support</li>
|
||||
</ul>
|
||||
<footer>
|
||||
<div>
|
||||
{% if user %}
|
||||
{% if (user.plan or 'free') == 'pro' %}
|
||||
<button class="secondary" disabled>Current Plan</button>
|
||||
<button class="btn-outline w-full" disabled>Current plan</button>
|
||||
{% else %}
|
||||
<form method="post" action="{{ url_for('billing.checkout', plan='pro') }}">
|
||||
<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>
|
||||
{% endif %}
|
||||
{% 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 %}
|
||||
</footer>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- FAQ -->
|
||||
<section style="margin-top: 4rem; max-width: 600px; margin-left: auto; margin-right: auto;">
|
||||
<h2>Frequently Asked Questions</h2>
|
||||
<section class="container-page pb-16 max-w-2xl mx-auto">
|
||||
<div class="section-heading">
|
||||
<h2>Frequently asked questions</h2>
|
||||
</div>
|
||||
|
||||
<details>
|
||||
<details class="faq-item">
|
||||
<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 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>
|
||||
<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>
|
||||
<summary>What commodities are available on Pro?</summary>
|
||||
<p>All 65 commodities tracked by USDA PSD, including coffee, cocoa, sugar, cotton, grains, oilseeds, and more.</p>
|
||||
<details class="faq-item">
|
||||
<summary>What format is the CSV export?</summary>
|
||||
<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>
|
||||
<summary>How do I cancel?</summary>
|
||||
<p>Cancel anytime from your dashboard settings. You keep access until the end of your billing period.</p>
|
||||
<details class="faq-item">
|
||||
<summary>How does the API work?</summary>
|
||||
<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>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@@ -1,26 +1,27 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Success! - {{ config.APP_NAME }}{% endblock %}
|
||||
{% block title %}You're All Set — {{ config.APP_NAME }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="container">
|
||||
<article style="max-width: 500px; margin: 4rem auto; text-align: center;">
|
||||
<header>
|
||||
<h1>🎉 Welcome Aboard!</h1>
|
||||
</header>
|
||||
<main class="container-page">
|
||||
<div class="auth-card text-center">
|
||||
<div class="mb-4">
|
||||
<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">
|
||||
<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>
|
||||
|
||||
<p>
|
||||
<a href="{{ url_for('dashboard.index') }}" role="button">Go to Dashboard</a>
|
||||
</p>
|
||||
<a href="{{ url_for('dashboard.index') }}" class="btn w-full mb-4">Go to Dashboard</a>
|
||||
|
||||
<hr>
|
||||
|
||||
<p><small>
|
||||
<p class="text-sm text-stone mt-4">
|
||||
Need to manage your subscription? Visit
|
||||
<a href="{{ url_for('dashboard.settings') }}">account settings</a>.
|
||||
</small></p>
|
||||
</article>
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,76 +1,98 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Country Comparison - {{ config.APP_NAME }}{% endblock %}
|
||||
{% block title %}Country Comparison — {{ config.APP_NAME }}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="container">
|
||||
<header>
|
||||
<main class="container-page py-8">
|
||||
<!-- Page Header -->
|
||||
<div class="page-header">
|
||||
<h1>Country Comparison</h1>
|
||||
<p>Compare coffee metrics across producing and consuming countries.</p>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="card mb-8">
|
||||
<form id="country-form" method="get" action="{{ url_for('dashboard.countries') }}">
|
||||
<div class="grid">
|
||||
<label>
|
||||
Metric
|
||||
<select name="metric" onchange="this.form.submit()">
|
||||
<div class="grid-2">
|
||||
<div>
|
||||
<label for="metric" class="form-label">Metric</label>
|
||||
<select name="metric" id="metric" class="form-input" onchange="this.form.submit()">
|
||||
{% for m in ["Production", "Exports", "Imports", "Ending_Stocks"] %}
|
||||
<option value="{{ m }}" {{ "selected" if metric == m }}>{{ m | replace("_", " ") }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Countries (select up to 10)
|
||||
<select name="country" multiple size="8" onchange="this.form.submit()">
|
||||
</div>
|
||||
<div>
|
||||
<label for="country" class="form-label">Countries (select up to 10)</label>
|
||||
<select name="country" id="country" class="form-input" multiple size="8" onchange="this.form.submit()">
|
||||
{% for c in all_countries %}
|
||||
<option value="{{ c.country_code }}" {{ "selected" if c.country_code in selected_codes }}>
|
||||
{{ c.country_name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Chart -->
|
||||
{% if comparison_data %}
|
||||
<section>
|
||||
<canvas id="comparisonChart" style="max-height: 500px;"></canvas>
|
||||
</section>
|
||||
<div class="chart-container mb-8">
|
||||
<canvas id="comparisonChart"></canvas>
|
||||
</div>
|
||||
{% else %}
|
||||
<article style="text-align: center; color: var(--muted-color);">
|
||||
<p>Select countries above to see the comparison chart.</p>
|
||||
</article>
|
||||
<div class="plan-gate mb-8">
|
||||
Select countries above to see the comparison chart.
|
||||
</div>
|
||||
{% 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>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
const COLORS = [
|
||||
'#2563eb', '#dc2626', '#16a34a', '#ca8a04', '#9333ea',
|
||||
'#0891b2', '#e11d48', '#65a30d', '#d97706', '#7c3aed'
|
||||
const CHART_COLORS = {
|
||||
copper: '#B45309',
|
||||
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 metric = {{ metric | tojson }};
|
||||
|
||||
if (rawData.length > 0) {
|
||||
// Group by country
|
||||
const byCountry = {};
|
||||
for (const row of rawData) {
|
||||
if (!byCountry[row.country_name]) byCountry[row.country_name] = [];
|
||||
byCountry[row.country_name].push(row);
|
||||
}
|
||||
|
||||
// Collect all years
|
||||
const allYears = [...new Set(rawData.map(r => r.market_year))].sort();
|
||||
|
||||
const datasets = Object.entries(byCountry).map(([name, rows], i) => {
|
||||
@@ -78,7 +100,7 @@ if (rawData.length > 0) {
|
||||
return {
|
||||
label: name,
|
||||
data: allYears.map(y => yearMap[y] ?? null),
|
||||
borderColor: COLORS[i % COLORS.length],
|
||||
borderColor: CHART_PALETTE[i % CHART_PALETTE.length],
|
||||
tension: 0.3,
|
||||
spanGaps: true
|
||||
};
|
||||
|
||||
@@ -1,100 +1,97 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Dashboard - {{ config.APP_NAME }}{% endblock %}
|
||||
{% block title %}Dashboard — {{ config.APP_NAME }}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="container">
|
||||
<header>
|
||||
<main class="container-page py-8">
|
||||
<!-- Page Header -->
|
||||
<div class="page-header">
|
||||
<h1>Coffee Dashboard</h1>
|
||||
<p>Welcome back{% if user.name %}, {{ user.name }}{% endif %}! Global coffee market data from USDA PSD.</p>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<!-- Key Metric Cards -->
|
||||
<div class="grid">
|
||||
<article>
|
||||
<header><small>Global Production (latest year)</small></header>
|
||||
<p style="font-size: 2rem; margin: 0;">
|
||||
<strong>{{ "{:,.0f}".format(latest.get("Production", 0)) }}</strong>
|
||||
</p>
|
||||
<small>1,000 60-kg bags</small>
|
||||
</article>
|
||||
<div class="grid-4 mb-8">
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Global Production (latest year)</div>
|
||||
<div class="metric-value">{{ "{:,.0f}".format(latest.get("Production", 0)) }}</div>
|
||||
<div class="metric-sub">1,000 60-kg bags</div>
|
||||
</div>
|
||||
|
||||
<article>
|
||||
<header><small>Stock-to-Use Ratio</small></header>
|
||||
<p style="font-size: 2rem; margin: 0;">
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Stock-to-Use Ratio</div>
|
||||
<div class="metric-value">
|
||||
{% 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 %}
|
||||
<strong>--</strong>
|
||||
--
|
||||
{% endif %}
|
||||
</p>
|
||||
<small>Ending stocks / consumption</small>
|
||||
</article>
|
||||
</div>
|
||||
<div class="metric-sub">Ending stocks / consumption</div>
|
||||
</div>
|
||||
|
||||
<article>
|
||||
<header><small>Trade Balance</small></header>
|
||||
<p style="font-size: 2rem; margin: 0;">
|
||||
<strong>{{ "{:,.0f}".format(latest.get("Exports", 0) - latest.get("Imports", 0)) }}</strong>
|
||||
</p>
|
||||
<small>Exports minus imports</small>
|
||||
</article>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Trade Balance</div>
|
||||
<div class="metric-value">{{ "{:,.0f}".format(latest.get("Exports", 0) - latest.get("Imports", 0)) }}</div>
|
||||
<div class="metric-sub">Exports minus imports</div>
|
||||
</div>
|
||||
|
||||
<article>
|
||||
<header><small>Your Plan</small></header>
|
||||
<p style="font-size: 2rem; margin: 0;"><strong>{{ plan | title }}</strong></p>
|
||||
<small>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Your Plan</div>
|
||||
<div class="metric-value">{{ plan | title }}</div>
|
||||
<div class="metric-sub">
|
||||
{% if plan == "free" %}
|
||||
<a href="{{ url_for('billing.pricing') }}">Upgrade for full history</a>
|
||||
{% else %}
|
||||
{{ stats.api_calls }} API calls (30d)
|
||||
{% endif %}
|
||||
</small>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Global Supply/Demand Time Series -->
|
||||
<section>
|
||||
<h2>Global Supply & Demand</h2>
|
||||
<div class="chart-container mb-8">
|
||||
<h2 class="text-xl mb-1">Global Supply & Demand</h2>
|
||||
{% 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 %}
|
||||
<canvas id="supplyDemandChart" style="max-height: 400px;"></canvas>
|
||||
</section>
|
||||
<canvas id="supplyDemandChart"></canvas>
|
||||
</div>
|
||||
|
||||
<!-- Stock-to-Use Ratio -->
|
||||
<section>
|
||||
<h2>Stock-to-Use Ratio Trend</h2>
|
||||
<canvas id="stuChart" style="max-height: 300px;"></canvas>
|
||||
</section>
|
||||
<div class="chart-container mb-8">
|
||||
<h2 class="text-xl mb-4">Stock-to-Use Ratio Trend</h2>
|
||||
<canvas id="stuChart"></canvas>
|
||||
</div>
|
||||
|
||||
<!-- Two-column: Top Producers + YoY Table -->
|
||||
<div class="grid">
|
||||
<section>
|
||||
<h2>Top Producing Countries</h2>
|
||||
<canvas id="topProducersChart" style="max-height: 400px;"></canvas>
|
||||
</section>
|
||||
<div class="grid-2 mb-8">
|
||||
<div class="chart-container">
|
||||
<h2 class="text-xl mb-4">Top Producing Countries</h2>
|
||||
<canvas id="topProducersChart"></canvas>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h2>Year-over-Year Production Change</h2>
|
||||
<div style="overflow-x: auto;">
|
||||
<table>
|
||||
<div class="card">
|
||||
<h2 class="text-xl mb-4">Year-over-Year Production Change</h2>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Country</th>
|
||||
<th style="text-align: right;">Production</th>
|
||||
<th style="text-align: right;">YoY %</th>
|
||||
<th class="text-right">Production</th>
|
||||
<th class="text-right">YoY %</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in yoy %}
|
||||
<tr>
|
||||
<td>{{ row.country_name }}</td>
|
||||
<td style="text-align: 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">{{ "{:,.0f}".format(row.Production) }}</td>
|
||||
<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 %}
|
||||
{{ "{:+.1f}%".format(row.Production_YoY_pct) }}
|
||||
{% else %}
|
||||
@@ -106,39 +103,58 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CSV Export (plan-gated) -->
|
||||
{% if plan != "free" %}
|
||||
<section>
|
||||
<a href="{{ url_for('api.commodity_metrics_csv', code=711100) }}" role="button" class="secondary outline">Export CSV</a>
|
||||
</section>
|
||||
<div class="mb-8">
|
||||
<a href="{{ url_for('api.commodity_metrics_csv', code=711100) }}" class="btn-outline">Export CSV</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<section>
|
||||
<p><small>CSV export available on Starter and Pro plans. <a href="{{ url_for('billing.pricing') }}">Upgrade</a></small></p>
|
||||
</section>
|
||||
<div class="plan-gate mb-8">CSV export available on Trader and Analyst plans. <a href="{{ url_for('billing.pricing') }}">Upgrade</a></div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<section>
|
||||
<div class="grid">
|
||||
<a href="{{ url_for('dashboard.countries') }}" role="button" class="secondary outline">Country Comparison</a>
|
||||
<a href="{{ url_for('dashboard.settings') }}" role="button" class="secondary outline">Settings</a>
|
||||
<a href="{{ url_for('dashboard.settings') }}#api-keys" role="button" class="secondary outline">API Keys</a>
|
||||
<div class="grid-3">
|
||||
<a href="{{ url_for('dashboard.countries') }}" class="btn-outline text-center">Country Comparison</a>
|
||||
<a href="{{ url_for('dashboard.settings') }}" class="btn-outline text-center">Settings</a>
|
||||
<a href="{{ url_for('dashboard.settings') }}#api-keys" class="btn-outline text-center">API Keys</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Chart colors
|
||||
const COLORS = [
|
||||
'#2563eb', '#dc2626', '#16a34a', '#ca8a04', '#9333ea',
|
||||
'#0891b2', '#e11d48', '#65a30d', '#d97706', '#7c3aed'
|
||||
// Brand chart colors
|
||||
const CHART_COLORS = {
|
||||
copper: '#B45309',
|
||||
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 --
|
||||
const tsData = {{ time_series | tojson }};
|
||||
if (tsData.length > 0) {
|
||||
@@ -147,11 +163,11 @@ if (tsData.length > 0) {
|
||||
data: {
|
||||
labels: tsData.map(r => r.market_year),
|
||||
datasets: [
|
||||
{label: 'Production', data: tsData.map(r => r.Production), borderColor: COLORS[0], tension: 0.3},
|
||||
{label: 'Exports', data: tsData.map(r => r.Exports), borderColor: COLORS[1], tension: 0.3},
|
||||
{label: 'Imports', data: tsData.map(r => r.Imports), borderColor: COLORS[2], tension: 0.3},
|
||||
{label: 'Ending Stocks', data: tsData.map(r => r.Ending_Stocks), borderColor: COLORS[3], tension: 0.3},
|
||||
{label: 'Total Distribution', data: tsData.map(r => r.Total_Distribution), borderColor: COLORS[4], 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: CHART_PALETTE[1], 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: CHART_PALETTE[3], tension: 0.3},
|
||||
{label: 'Total Distribution', data: tsData.map(r => r.Total_Distribution), borderColor: CHART_PALETTE[4], tension: 0.3},
|
||||
]
|
||||
},
|
||||
options: {
|
||||
@@ -172,8 +188,8 @@ if (stuData.length > 0) {
|
||||
datasets: [{
|
||||
label: 'Stock-to-Use Ratio (%)',
|
||||
data: stuData.map(r => r.Stock_to_Use_Ratio_pct),
|
||||
borderColor: COLORS[0],
|
||||
backgroundColor: 'rgba(37,99,235,0.1)',
|
||||
borderColor: CHART_COLORS.copper,
|
||||
backgroundColor: 'rgba(180, 83, 9, 0.08)',
|
||||
fill: true,
|
||||
tension: 0.3
|
||||
}]
|
||||
@@ -196,7 +212,7 @@ if (topData.length > 0) {
|
||||
datasets: [{
|
||||
label: 'Production',
|
||||
data: topData.map(r => r.Production),
|
||||
backgroundColor: COLORS[0]
|
||||
backgroundColor: CHART_COLORS.copper
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
|
||||
@@ -1,75 +1,75 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Settings - {{ config.APP_NAME }}{% endblock %}
|
||||
{% block title %}Settings — {{ config.APP_NAME }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="container">
|
||||
<header>
|
||||
<main class="container-page py-8">
|
||||
<!-- Page Header -->
|
||||
<div class="page-header">
|
||||
<h1>Settings</h1>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<!-- Profile Section -->
|
||||
<section>
|
||||
<h2>Profile</h2>
|
||||
<article>
|
||||
<div class="card">
|
||||
<div class="card-header">Profile</div>
|
||||
<form method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<label for="email">
|
||||
Email
|
||||
<input type="email" id="email" value="{{ user.email }}" disabled>
|
||||
<small>Email cannot be changed</small>
|
||||
</label>
|
||||
<div class="mb-4">
|
||||
<label for="email" class="form-label">Email</label>
|
||||
<input type="email" id="email" class="form-input bg-latte" value="{{ user.email }}" disabled>
|
||||
<p class="form-hint">Email cannot be changed</p>
|
||||
</div>
|
||||
|
||||
<label for="name">
|
||||
Name
|
||||
<input type="text" id="name" name="name" value="{{ user.name or '' }}" placeholder="Your name">
|
||||
</label>
|
||||
<div class="mb-4">
|
||||
<label for="name" class="form-label">Name</label>
|
||||
<input type="text" id="name" name="name" class="form-input" value="{{ user.name or '' }}" placeholder="Your name">
|
||||
</div>
|
||||
|
||||
<button type="submit">Save Changes</button>
|
||||
<button type="submit" class="btn">Save Changes</button>
|
||||
</form>
|
||||
</article>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Subscription Section -->
|
||||
<section>
|
||||
<h2>Subscription</h2>
|
||||
<article>
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<div class="card-header">Subscription</div>
|
||||
<div class="grid-3 mb-4">
|
||||
<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>
|
||||
<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>
|
||||
{% if user.current_period_end %}
|
||||
<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>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 1rem;">
|
||||
<div>
|
||||
{% 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() }}">
|
||||
<button type="submit" class="secondary">Manage Subscription</button>
|
||||
<button type="submit" class="btn-secondary">Manage Subscription</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<a href="{{ url_for('billing.pricing') }}" role="button">Upgrade Plan</a>
|
||||
<a href="{{ url_for('billing.pricing') }}" class="btn">Upgrade Plan</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- API Keys Section -->
|
||||
<section id="api-keys">
|
||||
<h2>API Keys</h2>
|
||||
<article>
|
||||
<p>API keys allow you to access the API programmatically.</p>
|
||||
<div class="card" id="api-keys">
|
||||
<div class="card-header">API Keys</div>
|
||||
<p class="text-sm text-stone mb-4">API keys allow you to access the API programmatically.</p>
|
||||
|
||||
{% if api_keys %}
|
||||
<table>
|
||||
<div class="overflow-x-auto mb-4">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
@@ -87,69 +87,71 @@
|
||||
<td>{{ key.scopes }}</td>
|
||||
<td>{{ key.created_at[:10] }}</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() }}">
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p><em>No API keys yet.</em></p>
|
||||
<p class="text-sm text-stone italic mb-4">No API keys yet.</p>
|
||||
{% endif %}
|
||||
|
||||
<details>
|
||||
<details class="faq-item">
|
||||
<summary>Create New API Key</summary>
|
||||
<div class="pt-2 pb-4">
|
||||
<form method="post" action="{{ url_for('dashboard.create_key') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<label for="key-name">
|
||||
Key Name
|
||||
<input type="text" id="key-name" name="name" placeholder="My API Key" required>
|
||||
</label>
|
||||
<div class="mb-4">
|
||||
<label for="key-name" class="form-label">Key Name</label>
|
||||
<input type="text" id="key-name" name="name" class="form-input" placeholder="My API Key" required>
|
||||
</div>
|
||||
|
||||
<fieldset>
|
||||
<legend>Scopes</legend>
|
||||
<label>
|
||||
<input type="checkbox" name="scopes" value="read" checked>
|
||||
<div class="mb-4">
|
||||
<span class="form-label">Scopes</span>
|
||||
<div class="flex gap-4 mt-1">
|
||||
<label class="flex items-center gap-2 text-sm text-stone-dark">
|
||||
<input type="checkbox" name="scopes" value="read" checked class="accent-copper">
|
||||
Read
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" name="scopes" value="write">
|
||||
<label class="flex items-center gap-2 text-sm text-stone-dark">
|
||||
<input type="checkbox" name="scopes" value="write" class="accent-copper">
|
||||
Write
|
||||
</label>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit">Create Key</button>
|
||||
<button type="submit" class="btn">Create Key</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
</article>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Danger Zone -->
|
||||
<section>
|
||||
<h2>Danger Zone</h2>
|
||||
<article style="border-color: var(--del-color);">
|
||||
<p>Once you delete your account, there is no going back. Please be certain.</p>
|
||||
<div class="danger-card">
|
||||
<h2 class="text-xl text-danger mb-2">Danger Zone</h2>
|
||||
<p class="text-sm text-stone mb-4">Once you delete your account, there is no going back. Please be certain.</p>
|
||||
|
||||
<details>
|
||||
<summary role="button" class="secondary outline" style="--pico-color: var(--del-color);">Delete Account</summary>
|
||||
<p>Are you sure? This will:</p>
|
||||
<ul>
|
||||
<details class="faq-item border-danger/30">
|
||||
<summary class="text-danger">Delete Account</summary>
|
||||
<div class="pt-2 pb-4">
|
||||
<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>Cancel your subscription</li>
|
||||
<li>Remove your API keys</li>
|
||||
</ul>
|
||||
<form method="post" action="{{ url_for('dashboard.delete_account') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="secondary" style="--pico-background-color: var(--del-color);">
|
||||
Yes, Delete My Account
|
||||
</button>
|
||||
<button type="submit" class="btn-danger">Yes, Delete My Account</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
</article>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,34 +1,48 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}About - {{ config.APP_NAME }}{% endblock %}
|
||||
{% block title %}About — {{ config.APP_NAME }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="container">
|
||||
<article style="max-width: 800px; margin: 0 auto;">
|
||||
<header style="text-align: center;">
|
||||
<h1>About {{ config.APP_NAME }}</h1>
|
||||
</header>
|
||||
<main>
|
||||
<!-- Hero -->
|
||||
<section class="hero">
|
||||
<div class="container-page">
|
||||
<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>
|
||||
<p>{{ config.APP_NAME }} was built with a simple philosophy: ship fast, stay simple.</p>
|
||||
<!-- Mission -->
|
||||
<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>
|
||||
<li><strong>SQLite for everything</strong> – It handles more than you think.</li>
|
||||
<li><strong>Server-rendered HTML</strong> – No build step, no hydration, no complexity.</li>
|
||||
<li><strong>Minimal dependencies</strong> – Fewer things to break.</li>
|
||||
<li><strong>Flat structure</strong> – Find things where you expect them.</li>
|
||||
<h2 class="text-2xl mb-4">Who this is for</h2>
|
||||
<ul class="space-y-2 text-stone mb-8">
|
||||
<li><strong class="text-espresso">Independent traders</strong> who need coffee supply & demand data without a terminal subscription.</li>
|
||||
<li><strong class="text-espresso">Roasters & importers</strong> tracking origin country production trends for sourcing decisions.</li>
|
||||
<li><strong class="text-espresso">Analysts & researchers</strong> who want clean, exportable coffee commodity data.</li>
|
||||
</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 style="text-align: center; margin-top: 3rem;">
|
||||
<a href="{{ url_for('auth.signup') }}" role="button">Get Started</a>
|
||||
<!-- CTA -->
|
||||
<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>
|
||||
</article>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,73 +1,129 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Features - {{ config.APP_NAME }}{% endblock %}
|
||||
{% block title %}Features — {{ config.APP_NAME }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="container">
|
||||
<header style="text-align: center; margin-bottom: 3rem;">
|
||||
<h1>Features</h1>
|
||||
<p>Coffee market intelligence built on USDA Production, Supply & Distribution data.</p>
|
||||
</header>
|
||||
|
||||
<section>
|
||||
<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>
|
||||
<main>
|
||||
<!-- Hero -->
|
||||
<section class="hero">
|
||||
<div class="container-page">
|
||||
<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>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section style="text-align: center; margin-top: 3rem;">
|
||||
<a href="{{ url_for('auth.signup') }}" role="button">Start Free</a>
|
||||
<a href="{{ url_for('billing.pricing') }}" role="button" class="secondary outline" style="margin-left: 1rem;">View Pricing</a>
|
||||
<!-- Feature 1: Supply & Demand -->
|
||||
<section class="container-page py-12">
|
||||
<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>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,91 +1,118 @@
|
||||
{% 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 %}
|
||||
<main class="container">
|
||||
<main>
|
||||
<!-- Hero -->
|
||||
<header style="text-align: center; padding: 4rem 0;">
|
||||
<h1>Coffee Market Intelligence<br>for Independent Traders</h1>
|
||||
<p style="font-size: 1.25rem; max-width: 640px; margin: 0 auto;">
|
||||
Track global supply and demand, compare producing countries, and spot trends
|
||||
with 18+ years of USDA data. No expensive terminal required.
|
||||
</p>
|
||||
<div style="margin-top: 2rem;">
|
||||
<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>
|
||||
<section class="hero">
|
||||
<div class="container-page">
|
||||
<h1 class="heading-display">Coffee market data without the Bloomberg price tag</h1>
|
||||
<p>18 years of USDA production, trade, and stock data for every coffee-producing country. Charts, exports, and API. Updated daily.</p>
|
||||
<div class="flex flex-col sm:flex-row gap-3 justify-center">
|
||||
<a href="{{ url_for('auth.signup') }}" class="btn">Start free — no credit card</a>
|
||||
<a href="{{ url_for('public.features') }}" class="btn-outline">See what's included</a>
|
||||
</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 -->
|
||||
<section style="padding: 4rem 0;">
|
||||
<h2 style="text-align: center;">What You Get</h2>
|
||||
|
||||
<div class="grid">
|
||||
<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>
|
||||
<section class="bg-latte py-16">
|
||||
<div class="container-page">
|
||||
<div class="section-heading">
|
||||
<h2>What you get</h2>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<article>
|
||||
<div class="grid-3">
|
||||
<div class="feature-card">
|
||||
<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>
|
||||
<p>Download data for your own models. Integrate with your trading tools via REST API.</p>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<p>Your data, your format. Download CSV files or integrate directly with your trading systems via REST API.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">⚡</div>
|
||||
<h3>Daily Refresh</h3>
|
||||
<p>Data pipeline runs daily against USDA PSD Online. Always current, always reliable.</p>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<h3>No Lock-in</h3>
|
||||
<p>Public USDA data, open methodology. You own your exports. Cancel anytime.</p>
|
||||
</article>
|
||||
<p>Our pipeline runs daily against USDA PSD Online. Latest data within hours of USDA publishing.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">🔓</div>
|
||||
<h3>Public Data, Open Method</h3>
|
||||
<p>No black box. Built on freely available USDA data with a fully transparent transformation pipeline.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- How It Works -->
|
||||
<section style="background: var(--card-background-color); border-radius: var(--border-radius); padding: 2rem;">
|
||||
<h2 style="text-align: center;">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>
|
||||
<section class="container-page py-16">
|
||||
<div class="section-heading">
|
||||
<h2>How it works</h2>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center;">
|
||||
<p style="font-size: 2rem;">2</p>
|
||||
<h4>Explore the Dashboard</h4>
|
||||
<p>Instant access to coffee supply/demand charts and country rankings.</p>
|
||||
<div class="grid-3 text-center">
|
||||
<div>
|
||||
<div class="step-number">1</div>
|
||||
<h4 class="mb-2">Sign up with your email</h4>
|
||||
<p class="text-sm text-stone">Magic link, no password. You're in within seconds.</p>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center;">
|
||||
<p style="font-size: 2rem;">3</p>
|
||||
<h4>Go Deeper</h4>
|
||||
<p>Upgrade for full history, CSV exports, and API access for your own models.</p>
|
||||
<div>
|
||||
<div class="step-number">2</div>
|
||||
<h4 class="mb-2">Explore the dashboard</h4>
|
||||
<p class="text-sm text-stone">Charts and metrics are ready. No setup, no configuration.</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>
|
||||
</section>
|
||||
|
||||
<!-- CTA -->
|
||||
<section style="text-align: center; padding: 4rem 0;">
|
||||
<h2>Ready to See the Data?</h2>
|
||||
<p>Free plan includes the last 5 years of coffee market data. No credit card required.</p>
|
||||
<a href="{{ url_for('auth.signup') }}" role="button">Start Free</a>
|
||||
<!-- Final CTA -->
|
||||
<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 before you decide</h2>
|
||||
<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>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,35 +1,36 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Privacy Policy - {{ config.APP_NAME }}{% endblock %}
|
||||
{% block title %}Privacy Policy — {{ config.APP_NAME }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="container">
|
||||
<article style="max-width: 800px; margin: 0 auto;">
|
||||
<header>
|
||||
<main class="container-page py-12">
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<div class="page-header">
|
||||
<h1>Privacy Policy</h1>
|
||||
<p><small>Last updated: January 2024</small></p>
|
||||
</header>
|
||||
<p class="text-sm text-stone">Last updated: January 2024</p>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h2>1. Information We Collect</h2>
|
||||
<p>We collect information you provide directly:</p>
|
||||
<ul>
|
||||
<div class="card">
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl mb-3">1. Information We Collect</h2>
|
||||
<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>Name (optional)</li>
|
||||
<li>Payment information (processed by Stripe)</li>
|
||||
<li>Payment information (processed by Paddle)</li>
|
||||
</ul>
|
||||
<p>We automatically collect:</p>
|
||||
<ul>
|
||||
<p class="text-stone mb-2">We automatically collect:</p>
|
||||
<ul class="list-disc pl-5 space-y-1 text-sm text-stone">
|
||||
<li>IP address</li>
|
||||
<li>Browser type</li>
|
||||
<li>Usage data</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>2. How We Use Information</h2>
|
||||
<p>We use your information to:</p>
|
||||
<ul>
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl mb-3">2. How We Use Information</h2>
|
||||
<p class="text-stone mb-2">We use your information to:</p>
|
||||
<ul class="list-disc pl-5 space-y-1 text-sm text-stone">
|
||||
<li>Provide and maintain the service</li>
|
||||
<li>Process payments</li>
|
||||
<li>Send transactional emails</li>
|
||||
@@ -38,34 +39,34 @@
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>3. Information Sharing</h2>
|
||||
<p>We do not sell your personal information. We may share information with:</p>
|
||||
<ul>
|
||||
<li>Service providers (Stripe for payments, Resend for email)</li>
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl mb-3">3. Information Sharing</h2>
|
||||
<p class="text-stone mb-2">We do not sell your personal information. We may share information with:</p>
|
||||
<ul class="list-disc pl-5 space-y-1 text-sm text-stone">
|
||||
<li>Service providers (Paddle for payments, Resend for email)</li>
|
||||
<li>Law enforcement when required by law</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>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>
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl mb-3">4. Data Retention</h2>
|
||||
<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>
|
||||
<h2>5. Security</h2>
|
||||
<p>We implement industry-standard security measures including encryption, secure sessions, and regular backups.</p>
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl mb-3">5. Security</h2>
|
||||
<p class="text-stone">We implement industry-standard security measures including encryption, secure sessions, and regular backups.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>6. Cookies</h2>
|
||||
<p>We use essential cookies for session management. We do not use tracking or advertising cookies.</p>
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl mb-3">6. Cookies</h2>
|
||||
<p class="text-stone">We use essential cookies for session management. We do not use tracking or advertising cookies.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>7. Your Rights</h2>
|
||||
<p>You have the right to:</p>
|
||||
<ul>
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl mb-3">7. Your Rights</h2>
|
||||
<p class="text-stone mb-2">You have the right to:</p>
|
||||
<ul class="list-disc pl-5 space-y-1 text-sm text-stone">
|
||||
<li>Access your data</li>
|
||||
<li>Correct inaccurate data</li>
|
||||
<li>Delete your account and data</li>
|
||||
@@ -73,20 +74,21 @@
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>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>
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl mb-3">8. GDPR Compliance</h2>
|
||||
<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>
|
||||
<h2>9. Changes</h2>
|
||||
<p>We may update this policy. We will notify you of significant changes via email.</p>
|
||||
<h2 class="text-xl mb-3">10. Contact</h2>
|
||||
<p class="text-stone">For privacy inquiries: {{ config.EMAIL_FROM }}</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>10. Contact</h2>
|
||||
<p>For privacy inquiries: {{ config.EMAIL_FROM }}</p>
|
||||
</section>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,34 +1,35 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Terms of Service - {{ config.APP_NAME }}{% endblock %}
|
||||
{% block title %}Terms of Service — {{ config.APP_NAME }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="container">
|
||||
<article style="max-width: 800px; margin: 0 auto;">
|
||||
<header>
|
||||
<main class="container-page py-12">
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<div class="page-header">
|
||||
<h1>Terms of Service</h1>
|
||||
<p><small>Last updated: January 2024</small></p>
|
||||
</header>
|
||||
<p class="text-sm text-stone">Last updated: January 2024</p>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h2>1. Acceptance of Terms</h2>
|
||||
<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>
|
||||
<div class="card">
|
||||
<section class="mb-8">
|
||||
<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>
|
||||
<h2>2. Description of Service</h2>
|
||||
<p>{{ config.APP_NAME }} provides a software-as-a-service platform. Features and functionality may change over time.</p>
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl mb-3">2. Description of Service</h2>
|
||||
<p class="text-stone">{{ config.APP_NAME }} provides a software-as-a-service platform. Features and functionality may change over time.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>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>
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl mb-3">3. User Accounts</h2>
|
||||
<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>
|
||||
<h2>4. Acceptable Use</h2>
|
||||
<p>You agree not to:</p>
|
||||
<ul>
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl mb-3">4. Acceptable Use</h2>
|
||||
<p class="text-stone mb-2">You agree not to:</p>
|
||||
<ul class="list-disc pl-5 space-y-1 text-sm text-stone">
|
||||
<li>Violate any laws or regulations</li>
|
||||
<li>Infringe on intellectual property rights</li>
|
||||
<li>Transmit harmful code or malware</li>
|
||||
@@ -37,35 +38,36 @@
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>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>
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl mb-3">5. Payment Terms</h2>
|
||||
<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>
|
||||
<h2>6. Termination</h2>
|
||||
<p>We may terminate or suspend your account for violations of these terms. You may cancel your account at any time.</p>
|
||||
<h2 class="text-xl mb-3">10. Contact</h2>
|
||||
<p class="text-stone">For questions about these terms, please contact us at {{ config.EMAIL_FROM }}.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{% 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);
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
/* ── BeanFlows Brand Theme ── */
|
||||
@theme {
|
||||
--font-display: "DM Sans", ui-sans-serif, system-ui, sans-serif;
|
||||
--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;
|
||||
|
||||
@@ -118,9 +118,34 @@
|
||||
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-links { display: none; }
|
||||
.nav-inner { justify-content: center; }
|
||||
.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 */
|
||||
@@ -128,6 +153,148 @@
|
||||
@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;
|
||||
|
||||
@@ -5,6 +5,11 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}{{ config.APP_NAME }}{% endblock %}</title>
|
||||
|
||||
<!-- Fonts -->
|
||||
<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">
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/output.css') }}">
|
||||
|
||||
@@ -15,6 +20,13 @@
|
||||
<nav class="nav-bar">
|
||||
<div class="nav-inner">
|
||||
<a href="{{ url_for('public.landing') }}" class="nav-logo">{{ config.APP_NAME }}</a>
|
||||
<button class="nav-hamburger" aria-label="Toggle menu" onclick="document.querySelector('.nav-links').classList.toggle('open')">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||
<line x1="3" y1="6" x2="21" y2="6"/>
|
||||
<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>
|
||||
@@ -51,15 +63,16 @@
|
||||
{% block content %}{% endblock %}
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="container-page mt-16 py-8 border-t border-parchment">
|
||||
<footer class="bg-latte mt-20">
|
||||
<div class="container-page py-12">
|
||||
<div class="grid-3">
|
||||
<div>
|
||||
<strong class="text-espresso">{{ config.APP_NAME }}</strong>
|
||||
<p class="text-sm text-stone mt-1">Coffee market intelligence for independent traders.</p>
|
||||
<strong class="heading-display text-lg">{{ config.APP_NAME }}</strong>
|
||||
<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>
|
||||
<strong class="text-espresso text-sm">Product</strong>
|
||||
<ul class="list-none p-0 mt-1 space-y-1 text-sm">
|
||||
<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('billing.pricing') }}">Pricing</a></li>
|
||||
<li><a href="{{ url_for('public.about') }}">About</a></li>
|
||||
@@ -67,15 +80,16 @@
|
||||
</div>
|
||||
<div>
|
||||
<strong class="text-espresso text-sm">Legal</strong>
|
||||
<ul class="list-none p-0 mt-1 space-y-1 text-sm">
|
||||
<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.privacy') }}">Privacy</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-center mt-8 text-xs text-stone">
|
||||
<div class="border-t border-parchment mt-8 pt-6 text-center text-xs text-stone">
|
||||
© {{ now.year }} {{ config.APP_NAME }}. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- HTMX -->
|
||||
|
||||
Reference in New Issue
Block a user