dd207eef9b
- Fix monthly/annual price toggle by moving x-data scope to outer div - Auto-create UserProfile in DB on first Authentik OAuth login - Update and expand tests for oauth_callback and _ensure_user_profile Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
380 lines
18 KiB
HTML
380 lines
18 KiB
HTML
{% 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">
|
||
From free exploration to unlimited enterprise processing — scale as your document workflows grow.
|
||
</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="/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.highlight %}
|
||
<a href="/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="/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">Dedicated</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="/login"
|
||
class="inline-block bg-white text-indigo-700 font-bold px-8 py-3 rounded-lg hover:bg-indigo-50 transition shadow">
|
||
Create your free account
|
||
</a>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|