Files
gh-christianlouis-docuelevate/frontend/templates/pricing.html
copilot-swe-agent[bot] db5f3d51d9 fix(ui): make pricing page CTAs link to /signup when local signup is enabled
All four CTA anchor elements on pricing.html were hardcoded to /login.
They now use /signup when allow_signup is True (MULTI_USER_ENABLED and
ALLOW_LOCAL_SIGNUP both true), falling back to /login when signup is
disabled. Bottom CTA button text also updates accordingly.

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
2026-03-07 21:18:17 +00:00

436 lines
21 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}Pricing & Plans DocuElevate{% endblock %}
{% block content %}
<div class="bg-gray-50 min-h-screen" x-data="{ annual: false }">
<!-- ── Hero ──────────────────────────────────────────────────────────────── -->
<div class="bg-gradient-to-br from-blue-700 via-indigo-700 to-purple-700 text-white py-20 px-4">
<div class="max-w-4xl mx-auto text-center">
<span class="inline-block bg-white/20 text-white text-xs font-semibold uppercase tracking-widest px-3 py-1 rounded-full mb-4">
Simple, transparent pricing
</span>
<h1 class="text-4xl sm:text-5xl font-extrabold mb-4 leading-tight">
Choose the plan that's right for you
</h1>
<p class="text-indigo-100 text-lg max-w-2xl mx-auto">
One price per person, per month — from casual exploration to power-user workflows. No team plans, no per-seat tiers.
</p>
<!-- Annual / Monthly toggle (cosmetic — actual billing handled separately) -->
<div class="mt-8 inline-flex items-center bg-white/10 rounded-full p-1 gap-1">
<button
@click="annual = false"
:class="!annual ? 'bg-white text-indigo-700 shadow' : 'text-white'"
class="px-5 py-2 rounded-full text-sm font-semibold transition-all duration-200"
>Monthly</button>
<button
@click="annual = true"
:class="annual ? 'bg-white text-indigo-700 shadow' : 'text-white'"
class="px-5 py-2 rounded-full text-sm font-semibold transition-all duration-200"
>
Annual <span class="ml-1 text-xs bg-green-400 text-green-900 rounded-full px-2 py-0.5 font-bold">Save ~20%</span>
</button>
</div>
</div>
</div>
<!-- ── Tier cards ────────────────────────────────────────────────────────── -->
<div class="max-w-7xl mx-auto px-4 -mt-10 pb-20">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 items-stretch">
{% for tier in tiers %}
<div class="relative flex flex-col rounded-2xl shadow-lg overflow-hidden
{% if tier.highlight %}ring-4 ring-indigo-500 scale-105 z-10 bg-white{% else %}bg-white{% endif %}
transition-transform duration-200 hover:shadow-xl"
>
<!-- Popular badge -->
{% if tier.badge %}
<div class="absolute top-0 right-0 mt-4 mr-4">
<span class="
{% if tier.badge == 'Most Popular' %}bg-indigo-600 text-white
{% elif tier.badge == 'Best Value' %}bg-green-600 text-white
{% else %}bg-gray-600 text-white{% endif %}
text-xs font-bold uppercase tracking-wide px-3 py-1 rounded-full shadow"
>
{{ tier.badge }}
</span>
</div>
{% endif %}
<!-- Card header -->
<div class="p-6 pb-4 border-b border-gray-100">
<h2 class="text-xl font-bold text-gray-900">{{ tier.name }}</h2>
<p class="text-gray-500 text-sm mt-1">{{ tier.tagline }}</p>
<!-- Price -->
<div class="mt-4">
<template x-if="!annual">
<div>
{% if tier.price_monthly == 0 %}
<span class="text-4xl font-extrabold text-gray-900">Free</span>
{% else %}
<span class="text-4xl font-extrabold text-gray-900">${{ tier.price_monthly }}</span>
<span class="text-gray-500 text-sm">/month</span>
{% endif %}
</div>
</template>
<template x-if="annual">
<div>
{% if tier.price_yearly == 0 %}
<span class="text-4xl font-extrabold text-gray-900">Free</span>
{% else %}
<span class="text-4xl font-extrabold text-gray-900">${{ tier.price_yearly }}</span>
<span class="text-gray-500 text-sm">/year</span>
<div class="text-xs text-green-600 font-semibold mt-0.5">
~${{ "%.2f"|format(tier.price_yearly / 12) }}/month billed annually
</div>
{% endif %}
</div>
</template>
</div>
<!-- Free trial badge for paid tiers -->
{% if tier.trial_days > 0 %}
<div class="mt-3 inline-flex items-center gap-1 text-xs font-semibold text-emerald-700 bg-emerald-50 border border-emerald-200 rounded-full px-3 py-1">
<i class="fas fa-gift" aria-hidden="true"></i>
{{ tier.trial_days }}-day free trial — no credit card required
</div>
{% endif %}
</div>
<!-- Features list -->
<div class="p-6 flex-1">
<ul class="space-y-3 text-sm text-gray-600">
{% for feature in tier.features %}
<li class="flex items-start gap-2">
<i class="fas fa-check-circle text-green-500 mt-0.5 flex-shrink-0" aria-hidden="true"></i>
<span>{{ feature }}</span>
</li>
{% endfor %}
</ul>
</div>
<!-- CTA button -->
<div class="p-6 pt-0">
{% if tier.id == 'free' %}
<a href="{{ '/signup' if allow_signup else '/login' }}"
class="block w-full text-center py-3 px-4 rounded-lg border-2 border-blue-600 text-blue-600 font-semibold hover:bg-blue-50 transition"
>{{ tier.cta }}</a>
{% elif tier.stripe_price_id_monthly or tier.stripe_price_id_yearly %}
<button
type="button"
onclick="startCheckout('{{ tier.id }}')"
class="block w-full text-center py-3 px-4 rounded-lg {% if tier.highlight %}bg-indigo-600 text-white font-semibold hover:bg-indigo-700 transition shadow-md{% else %}bg-blue-600 text-white font-semibold hover:bg-blue-700 transition{% endif %}"
style="min-height:44px;"
>{{ tier.cta }}</button>
{% elif tier.highlight %}
<a href="{{ '/signup' if allow_signup else '/login' }}"
class="block w-full text-center py-3 px-4 rounded-lg bg-indigo-600 text-white font-semibold hover:bg-indigo-700 transition shadow-md"
>{{ tier.cta }}</a>
{% else %}
<a href="{{ '/signup' if allow_signup else '/login' }}"
class="block w-full text-center py-3 px-4 rounded-lg bg-blue-600 text-white font-semibold hover:bg-blue-700 transition"
>{{ tier.cta }}</a>
{% endif %}
</div>
</div>
{% endfor %}
</div>
<!-- ── Comparison table ───────────────────────────────────────────────── -->
<div class="mt-20">
<h2 class="text-2xl font-bold text-gray-900 text-center mb-8">Full feature comparison</h2>
<div class="overflow-x-auto rounded-2xl shadow">
<table class="min-w-full bg-white divide-y divide-gray-200" aria-label="Plan comparison">
<thead>
<tr class="bg-gray-50">
<th scope="col" class="px-6 py-4 text-left text-sm font-semibold text-gray-700 w-1/3">Feature</th>
{% for tier in tiers %}
<th scope="col" class="px-4 py-4 text-center text-sm font-semibold
{% if tier.highlight %}text-indigo-700{% else %}text-gray-700{% endif %}">
{{ tier.name }}
</th>
{% endfor %}
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
<tr class="bg-gray-50/50">
<th scope="row" colspan="5" class="px-6 py-2 text-xs font-bold text-gray-400 uppercase tracking-wider text-left">
File Processing
</th>
</tr>
<tr class="hover:bg-indigo-50/30 transition">
<td class="px-6 py-3 text-sm text-gray-700">Lifetime file limit</td>
{% for tier in tiers %}
<td class="px-4 py-3 text-center text-sm">
{% if tier.lifetime_file_limit == 0 %}
<span class="font-semibold text-green-600">Unlimited</span>
{% else %}
<span class="font-medium text-gray-800">{{ tier.lifetime_file_limit }}</span>
{% endif %}
</td>
{% endfor %}
</tr>
<tr class="hover:bg-indigo-50/30 transition">
<td class="px-6 py-3 text-sm text-gray-700">Files per month</td>
{% for tier in tiers %}
<td class="px-4 py-3 text-center text-sm">
{% if tier.monthly_upload_limit == 0 %}
<span class="font-semibold text-green-600">Unlimited</span>
{% else %}
<span class="font-medium text-gray-800">{{ tier.monthly_upload_limit }}</span>
{% endif %}
</td>
{% endfor %}
</tr>
<tr class="hover:bg-indigo-50/30 transition">
<td class="px-6 py-3 text-sm text-gray-700">Max file size</td>
{% for tier in tiers %}
<td class="px-4 py-3 text-center text-sm">
{% if tier.max_file_size_mb == 0 %}
<span class="font-semibold text-green-600">Unlimited</span>
{% else %}
<span class="font-medium text-gray-800">{{ tier.max_file_size_mb }} MB</span>
{% endif %}
</td>
{% endfor %}
</tr>
<tr class="bg-gray-50/50">
<th scope="row" colspan="5" class="px-6 py-2 text-xs font-bold text-gray-400 uppercase tracking-wider text-left">
OCR & AI
</th>
</tr>
<tr class="hover:bg-indigo-50/30 transition">
<td class="px-6 py-3 text-sm text-gray-700">OCR pages / month</td>
{% for tier in tiers %}
<td class="px-4 py-3 text-center text-sm">
{% if tier.max_ocr_pages_monthly == 0 %}
<span class="font-semibold text-green-600">Unlimited</span>
{% else %}
<span class="font-medium text-gray-800">{{ "{:,}".format(tier.max_ocr_pages_monthly) }}</span>
{% endif %}
</td>
{% endfor %}
</tr>
<tr class="hover:bg-indigo-50/30 transition">
<td class="px-6 py-3 text-sm text-gray-700">AI metadata extraction</td>
<td class="px-4 py-3 text-center">
<span class="text-xs text-gray-500">Basic</span>
</td>
{% for tier in tiers[1:] %}
<td class="px-4 py-3 text-center">
<i class="fas fa-check text-green-500" aria-label="Included"></i>
</td>
{% endfor %}
</tr>
<tr class="bg-gray-50/50">
<th scope="row" colspan="5" class="px-6 py-2 text-xs font-bold text-gray-400 uppercase tracking-wider text-left">
Integrations
</th>
</tr>
<tr class="hover:bg-indigo-50/30 transition">
<td class="px-6 py-3 text-sm text-gray-700">Storage destinations</td>
{% for tier in tiers %}
<td class="px-4 py-3 text-center text-sm">
{% if tier.max_storage_destinations == 0 %}
<span class="font-semibold text-green-600">Unlimited</span>
{% else %}
<span class="font-medium text-gray-800">{{ tier.max_storage_destinations }}</span>
{% endif %}
</td>
{% endfor %}
</tr>
<tr class="hover:bg-indigo-50/30 transition">
<td class="px-6 py-3 text-sm text-gray-700">Email ingestion</td>
<td class="px-4 py-3 text-center">
<i class="fas fa-times text-gray-300" aria-label="Not included"></i>
</td>
{% for tier in tiers[1:] %}
<td class="px-4 py-3 text-center">
<i class="fas fa-check text-green-500" aria-label="Included"></i>
</td>
{% endfor %}
</tr>
<tr class="hover:bg-indigo-50/30 transition">
<td class="px-6 py-3 text-sm text-gray-700">Mailboxes (ingestion sources)</td>
{% for tier in tiers %}
<td class="px-4 py-3 text-center text-sm">
{% if tier.max_mailboxes == 0 and tier.id == 'free' %}
<i class="fas fa-times text-gray-300" aria-label="Not included"></i>
{% elif tier.max_mailboxes == 0 %}
<span class="font-semibold text-green-600">Unlimited</span>
{% else %}
<span class="font-medium text-gray-800">{{ tier.max_mailboxes }}</span>
{% endif %}
</td>
{% endfor %}
</tr>
<tr class="hover:bg-indigo-50/30 transition">
<td class="px-6 py-3 text-sm text-gray-700">Webhooks</td>
<td class="px-4 py-3 text-center">
<i class="fas fa-times text-gray-300" aria-label="Not included"></i>
</td>
<td class="px-4 py-3 text-center">
<i class="fas fa-times text-gray-300" aria-label="Not included"></i>
</td>
<td class="px-4 py-3 text-center">
<i class="fas fa-check text-green-500" aria-label="Included"></i>
</td>
<td class="px-4 py-3 text-center">
<i class="fas fa-check text-green-500" aria-label="Included"></i>
</td>
</tr>
<tr class="hover:bg-indigo-50/30 transition">
<td class="px-6 py-3 text-sm text-gray-700">API access</td>
<td class="px-4 py-3 text-center">
<i class="fas fa-times text-gray-300" aria-label="Not included"></i>
</td>
{% for tier in tiers[1:] %}
<td class="px-4 py-3 text-center">
<i class="fas fa-check text-green-500" aria-label="Included"></i>
</td>
{% endfor %}
</tr>
<tr class="bg-gray-50/50">
<th scope="row" colspan="5" class="px-6 py-2 text-xs font-bold text-gray-400 uppercase tracking-wider text-left">
Support
</th>
</tr>
<tr class="hover:bg-indigo-50/30 transition">
<td class="px-6 py-3 text-sm text-gray-700">Support level</td>
<td class="px-4 py-3 text-center text-xs text-gray-500">Community</td>
<td class="px-4 py-3 text-center text-xs text-gray-700">Email</td>
<td class="px-4 py-3 text-center text-xs text-gray-700 font-medium">Priority email</td>
<td class="px-4 py-3 text-center text-xs text-indigo-700 font-bold">Priority</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- ── FAQ ───────────────────────────────────────────────────────────── -->
<div class="mt-20 max-w-3xl mx-auto">
<h2 class="text-2xl font-bold text-gray-900 text-center mb-8">Frequently asked questions</h2>
<div class="space-y-4" x-data="{ open: null }">
{% set faqs = [
("What counts as a 'processed file'?",
"Every document you upload and run through the DocuElevate pipeline counts as one processed file — including OCR, AI metadata extraction, and storage delivery."),
("What happens when I reach the Free tier lifetime limit?",
"Once your 50-file lifetime quota is reached you will see a friendly upgrade prompt on the upload page and any further upload attempts will return a payment-required error until you upgrade to a paid plan."),
("Can I change my plan at any time?",
"Yes. Upgrades take effect immediately. Downgrades take effect at the start of the next billing cycle. Unused quota does not roll over between billing periods."),
("Is there an annual discount?",
"Yes — paying annually saves approximately 20 % compared to monthly billing (≈ 2½ months free). The exact annual price and per-month equivalent are shown when you toggle to Annual above."),
("Is there a free trial for paid plans?",
"Yes! All three paid plans include a 30-day free trial — no credit card required. You can upgrade from the Free tier or start a trial directly from any paid plan card above."),
("What is a 'storage destination'?",
"A storage destination is any cloud or self-hosted storage you configure as an output — Dropbox, Google Drive, OneDrive, Nextcloud, S3, SFTP, FTP, WebDAV, or Paperless-ngx each count as one destination."),
("What is a 'mailbox'?",
"A mailbox is an email address DocuElevate monitors for incoming documents. Any attachment arriving at a configured mailbox is automatically processed through the pipeline. The Free tier does not include email ingestion."),
("Are prices inclusive of VAT?",
"Listed prices are exclusive of VAT. Customers in Germany are charged 19 % Mehrwertsteuer (MwSt) at checkout. EU business customers outside Germany apply the reverse-charge mechanism. Non-EU customers are not subject to German VAT."),
] %}
{% for q, a in faqs %}
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<button
@click="open = (open === {{ loop.index0 }}) ? null : {{ loop.index0 }}"
class="w-full flex justify-between items-center px-6 py-4 text-left text-sm font-semibold text-gray-800 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500"
:aria-expanded="open === {{ loop.index0 }}"
>
<span>{{ q }}</span>
<i class="fas text-indigo-400 ml-4 flex-shrink-0 transition-transform duration-200"
:class="open === {{ loop.index0 }} ? 'fa-chevron-up' : 'fa-chevron-down'"
aria-hidden="true"></i>
</button>
<div x-show="open === {{ loop.index0 }}" x-transition class="px-6 pb-4 text-sm text-gray-600">
{{ a }}
</div>
</div>
{% endfor %}
</div>
</div>
<!-- ── CTA strip ─────────────────────────────────────────────────────── -->
<div class="mt-20 rounded-2xl bg-gradient-to-r from-blue-600 to-indigo-700 text-white p-10 text-center shadow-xl">
<h2 class="text-2xl font-bold mb-2">Ready to get started?</h2>
<p class="text-indigo-100 mb-6">Start with the free tier — no credit card required.</p>
<a href="{{ '/signup' if allow_signup else '/login' }}"
class="inline-block bg-white text-indigo-700 font-bold px-8 py-3 rounded-lg hover:bg-indigo-50 transition shadow">
{% if allow_signup %}Create your free account{% else %}Sign in{% endif %}
</a>
</div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
/**
* Initiate a Stripe Checkout session for the given plan.
* Uses the currently-selected billing cycle toggle (monthly/yearly).
*
* @param {string} planId - The plan identifier (e.g. 'starter', 'professional').
*/
async function startCheckout(planId) {
const cycleEl = document.querySelector('[data-billing-cycle]');
const billingCycle = (cycleEl && cycleEl.dataset.billingCycle) ? cycleEl.dataset.billingCycle : 'monthly';
// Clear previous error
const errEl = document.getElementById('checkout-error');
if (errEl) { errEl.textContent = ''; errEl.hidden = true; }
try {
const resp = await fetch('/api/billing/create-checkout-session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ plan_id: planId, billing_cycle: billingCycle })
});
if (resp.status === 401) {
window.location.href = '/login?message=Please+sign+in+to+subscribe';
return;
}
if (resp.ok) {
const data = await resp.json();
if (data.checkout_url) {
window.location.href = data.checkout_url;
return;
}
}
const data = await resp.json().catch(() => ({}));
const msg = data.detail || 'Unable to start checkout. Please try again.';
if (errEl) { errEl.textContent = msg; errEl.hidden = false; }
} catch (e) {
if (errEl) { errEl.textContent = 'Network error. Please try again.'; errEl.hidden = false; }
}
}
</script>
<div id="checkout-error"
role="alert"
aria-live="assertive"
hidden
class="fixed bottom-6 left-1/2 -translate-x-1/2 bg-red-100 border border-red-400 text-red-700 px-6 py-3 rounded-lg shadow-lg text-sm z-50">
</div>
{% endblock %}