5a025576c4
- Add 155 new translation keys to en.json (common, nav, integrations, profile, subscription sections) - Add window.__i18n script block to base.html for user menu strings - Replace all hardcoded English strings in common.js with window.__i18n lookups using safe fallback pattern - Internationalize profile.html (page title, headings, labels, hints, placeholders, theme options) - Internationalize subscription.html (page title, plan cards, usage stats, plan actions, badges) - Internationalize integrations_dashboard.html (header, quota bars, modal, all form fields, action buttons, webhook section, delete modal) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
354 lines
16 KiB
HTML
354 lines
16 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}{{ _("subscription.page_title") }}{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container mx-auto px-4 py-8 max-w-4xl">
|
|
|
|
<!-- Header -->
|
|
<div class="mb-8">
|
|
<h1 class="text-3xl font-bold text-gray-900 flex items-center gap-3">
|
|
<i class="fas fa-layer-group text-indigo-500" aria-hidden="true"></i>
|
|
{{ _("subscription.heading") }}
|
|
</h1>
|
|
<p class="text-gray-500 text-sm mt-1">{{ _("subscription.subtitle") }}</p>
|
|
</div>
|
|
|
|
<!-- Flash messages -->
|
|
<div id="flash-container" aria-live="polite" aria-atomic="true"></div>
|
|
|
|
{% if not multi_user_enabled %}
|
|
<!-- Single-user notice -->
|
|
<div class="bg-blue-50 border border-blue-200 rounded-xl p-6 flex items-start gap-4">
|
|
<i class="fas fa-info-circle text-blue-500 text-2xl flex-shrink-0 mt-0.5" aria-hidden="true"></i>
|
|
<div>
|
|
<p class="font-semibold text-blue-800">{{ _("subscription.single_user_title") }}</p>
|
|
<p class="text-blue-700 text-sm mt-1">
|
|
{{ _("subscription.single_user_body_before_link") }}
|
|
<a href="/settings" class="underline hover:text-blue-900">{{ _("nav.settings") }}</a>{{ _("subscription.single_user_body_after_link") }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{% else %}
|
|
|
|
<!-- Pending-change banner -->
|
|
{% if pending_tier_id %}
|
|
<div class="bg-amber-50 border border-amber-300 rounded-xl p-4 mb-6 flex items-start gap-3">
|
|
<i class="fas fa-clock text-amber-500 text-xl flex-shrink-0 mt-0.5" aria-hidden="true"></i>
|
|
<div class="flex-1">
|
|
<p class="font-semibold text-amber-800">{{ _("subscription.pending_title") }}</p>
|
|
<p class="text-amber-700 text-sm mt-1">
|
|
{{ _("subscription.pending_will_change") }} <strong>{{ pending_tier.name }}</strong>
|
|
{{ _("subscription.pending_on") }} <strong>{% if pending_date %}{{ pending_date.strftime('%B') }} {{ pending_date.day }}, {{ pending_date.year }}{% endif %}</strong>.
|
|
{{ _("subscription.pending_benefits") }}
|
|
</p>
|
|
</div>
|
|
<button type="button" onclick="cancelPendingChange()"
|
|
class="ml-auto flex-shrink-0 text-sm text-amber-700 underline hover:text-amber-900 font-medium"
|
|
aria-label="{{ _('subscription.cancel_pending') }}">
|
|
{{ _("subscription.cancel_pending") }}
|
|
</button>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Current plan card -->
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
|
|
|
<!-- Plan badge -->
|
|
<div class="bg-white rounded-xl shadow p-6 flex flex-col items-center text-center
|
|
{% if tier_id == 'professional' %}ring-2 ring-indigo-500{% endif %}">
|
|
<div class="h-14 w-14 rounded-full
|
|
{% if tier_id == 'free' %}bg-gray-100{% elif tier_id == 'starter' %}bg-blue-100{% elif tier_id == 'professional' %}bg-indigo-100{% else %}bg-purple-100{% endif %}
|
|
flex items-center justify-center mb-3">
|
|
<i class="fas
|
|
{% if tier_id == 'free' %}fa-seedling text-gray-500
|
|
{% elif tier_id == 'starter' %}fa-rocket text-blue-600
|
|
{% elif tier_id == 'professional' %}fa-star text-indigo-600
|
|
{% else %}fa-bolt text-purple-600{% endif %}
|
|
text-2xl" aria-hidden="true"></i>
|
|
</div>
|
|
<p class="text-xs font-semibold text-gray-400 uppercase tracking-widest">{{ _("subscription.current_plan") }}</p>
|
|
<p class="text-2xl font-extrabold text-gray-900 mt-1">{{ tier.name }}</p>
|
|
<p class="text-gray-500 text-sm mt-1">{{ tier.tagline }}</p>
|
|
{% if tier.price_monthly > 0 %}
|
|
<p class="mt-3 text-lg font-bold text-indigo-600">${{ tier.price_monthly }}<span class="text-sm font-normal text-gray-400">{{ _("subscription.per_month") }}</span></p>
|
|
{% else %}
|
|
<p class="mt-3 text-lg font-bold text-gray-500">{{ _("subscription.free") }}</p>
|
|
{% endif %}
|
|
{% if period_start %}
|
|
<p class="text-xs text-gray-400 mt-2">
|
|
<i class="fas fa-calendar-alt mr-1" aria-hidden="true"></i>
|
|
{{ _("subscription.since") }} {{ period_start.strftime('%b') }} {{ period_start.day }}, {{ period_start.year }}
|
|
</p>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Usage this period -->
|
|
{% if usage %}
|
|
<div class="md:col-span-2 bg-white rounded-xl shadow p-6">
|
|
<h2 class="text-sm font-semibold text-gray-500 uppercase tracking-widest mb-4">{{ _("subscription.usage_heading") }}</h2>
|
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
|
|
|
<!-- Lifetime -->
|
|
<div class="text-center">
|
|
<p class="text-3xl font-extrabold text-gray-900">{{ usage.lifetime }}</p>
|
|
<p class="text-xs text-gray-500 mt-1">{{ _("subscription.lifetime_files") }}</p>
|
|
{% if tier.lifetime_file_limit > 0 %}
|
|
<div class="mt-2">
|
|
{% set lifetime_pct = ((usage.lifetime / tier.lifetime_file_limit) * 100) | int %}
|
|
<div class="w-full bg-gray-200 rounded-full h-1.5" role="progressbar" aria-valuemin="0"
|
|
aria-valuenow="{{ usage.lifetime }}" aria-valuemax="{{ tier.lifetime_file_limit }}">
|
|
<div class="h-1.5 rounded-full
|
|
{% if lifetime_pct >= 90 %}bg-red-500{% elif lifetime_pct >= 70 %}bg-yellow-500{% else %}bg-green-500{% endif %}"
|
|
style="width: {{ [lifetime_pct, 100] | min }}%"></div>
|
|
</div>
|
|
<p class="text-xs text-gray-400 mt-1">{{ usage.lifetime }} / {{ tier.lifetime_file_limit }}</p>
|
|
</div>
|
|
{% else %}
|
|
<p class="text-xs text-green-600 mt-1">{{ _("subscription.unlimited") }}</p>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Today -->
|
|
<div class="text-center">
|
|
<p class="text-3xl font-extrabold text-gray-900">{{ usage.today }}</p>
|
|
<p class="text-xs text-gray-500 mt-1">{{ _("subscription.today_files") }}</p>
|
|
{% if tier.daily_upload_limit > 0 %}
|
|
<div class="mt-2">
|
|
{% set today_pct = ((usage.today / tier.daily_upload_limit) * 100) | int %}
|
|
<div class="w-full bg-gray-200 rounded-full h-1.5" role="progressbar" aria-valuemin="0"
|
|
aria-valuenow="{{ usage.today }}" aria-valuemax="{{ tier.daily_upload_limit }}">
|
|
<div class="h-1.5 rounded-full
|
|
{% if today_pct >= 90 %}bg-red-500{% elif today_pct >= 70 %}bg-yellow-500{% else %}bg-blue-500{% endif %}"
|
|
style="width: {{ [today_pct, 100] | min }}%"></div>
|
|
</div>
|
|
<p class="text-xs text-gray-400 mt-1">{{ usage.today }} / {{ tier.daily_upload_limit }} {{ _("subscription.today_label") }}</p>
|
|
</div>
|
|
{% else %}
|
|
<p class="text-xs text-green-600 mt-1">{{ _("subscription.unlimited") }}</p>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- This month -->
|
|
<div class="text-center">
|
|
<p class="text-3xl font-extrabold text-gray-900">{{ usage.month }}</p>
|
|
<p class="text-xs text-gray-500 mt-1">{{ _("subscription.month_files") }}</p>
|
|
{% if tier.monthly_upload_limit > 0 %}
|
|
<div class="mt-2">
|
|
{% set month_pct = ((usage.month / tier.monthly_upload_limit) * 100) | int %}
|
|
<div class="w-full bg-gray-200 rounded-full h-1.5" role="progressbar" aria-valuemin="0"
|
|
aria-valuenow="{{ usage.month }}" aria-valuemax="{{ tier.monthly_upload_limit }}">
|
|
<div class="h-1.5 rounded-full
|
|
{% if month_pct >= 90 %}bg-red-500{% elif month_pct >= 70 %}bg-yellow-500{% else %}bg-indigo-500{% endif %}"
|
|
style="width: {{ [month_pct, 100] | min }}%"></div>
|
|
</div>
|
|
<p class="text-xs text-gray-400 mt-1">{{ usage.month }} / {{ tier.monthly_upload_limit }} {{ _("subscription.this_month_label") }}</p>
|
|
</div>
|
|
{% else %}
|
|
<p class="text-xs text-green-600 mt-1">{{ _("subscription.unlimited") }}</p>
|
|
{% endif %}
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
</div>
|
|
|
|
<!-- Plan features included -->
|
|
<div class="bg-white rounded-xl shadow p-6 mb-8">
|
|
<h2 class="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
|
|
<i class="fas fa-list-check text-green-500" aria-hidden="true"></i>
|
|
{{ _("subscription.features_heading") }}
|
|
</h2>
|
|
<ul class="grid grid-cols-1 sm:grid-cols-2 gap-2 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>
|
|
|
|
<!-- All available plans -->
|
|
<div class="mb-8">
|
|
<h2 class="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
|
|
<i class="fas fa-layer-group text-indigo-500" aria-hidden="true"></i>
|
|
{{ _("subscription.available_plans_heading") }}
|
|
</h2>
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
{% for t in all_tiers %}
|
|
{% set t_rank = tier_order.index(t.id) %}
|
|
{% set current_rank = tier_order.index(tier_id) %}
|
|
{% set is_current = (t.id == tier_id) %}
|
|
{% set is_pending = (t.id == pending_tier_id) %}
|
|
{% set is_upgrade = (t_rank > current_rank) %}
|
|
{% set is_downgrade = (t_rank < current_rank) %}
|
|
<div class="bg-white rounded-xl border-2
|
|
{% if is_current %}border-indigo-500 ring-2 ring-indigo-200{% elif t.highlight %}border-indigo-300{% else %}border-gray-200{% endif %}
|
|
p-5 flex flex-col justify-between hover:shadow-md transition relative">
|
|
|
|
{% if is_current %}
|
|
<span class="absolute top-3 right-3 text-xs bg-indigo-100 text-indigo-700 font-semibold rounded-full px-2 py-0.5">
|
|
{{ _("subscription.current_badge") }}
|
|
</span>
|
|
{% elif is_pending %}
|
|
<span class="absolute top-3 right-3 text-xs bg-amber-100 text-amber-700 font-semibold rounded-full px-2 py-0.5">
|
|
{{ _("subscription.pending_badge") }}
|
|
</span>
|
|
{% elif t.badge %}
|
|
<span class="absolute top-3 right-3 text-xs bg-indigo-100 text-indigo-700 font-semibold rounded-full px-2 py-0.5">
|
|
{{ t.badge }}
|
|
</span>
|
|
{% endif %}
|
|
|
|
<div>
|
|
<p class="font-bold text-gray-900 pr-16">{{ t.name }}</p>
|
|
<p class="text-gray-500 text-xs mt-1 mb-3">{{ t.tagline }}</p>
|
|
{% if t.price_monthly > 0 %}
|
|
<p class="text-xl font-extrabold text-gray-900">${{ t.price_monthly }}<span class="text-sm font-normal text-gray-400">{{ _("subscription.per_mo") }}</span></p>
|
|
{% else %}
|
|
<p class="text-xl font-extrabold text-gray-500">{{ _("subscription.free") }}</p>
|
|
{% endif %}
|
|
<ul class="mt-3 space-y-1 text-xs text-gray-600">
|
|
{% for feature in t.features[:3] %}
|
|
<li class="flex items-start gap-1.5">
|
|
<i class="fas fa-check-circle text-green-500 mt-0.5 flex-shrink-0" aria-hidden="true"></i>
|
|
{{ feature }}
|
|
</li>
|
|
{% endfor %}
|
|
{% if t.features | length > 3 %}
|
|
<li class="text-indigo-500 font-medium">+ {{ (t.features | length) - 3 }} {{ _("subscription.more_features_label") }}</li>
|
|
{% endif %}
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="mt-4">
|
|
{% if is_current and not pending_tier_id %}
|
|
<span class="block text-center py-2 px-4 rounded-lg bg-gray-100 text-gray-400 text-sm font-semibold cursor-default">
|
|
{{ _("subscription.current_plan_cta") }}
|
|
</span>
|
|
{% elif is_current and pending_tier_id %}
|
|
<!-- Keep current plan (cancel pending downgrade) -->
|
|
<button type="button"
|
|
onclick="changePlan('{{ t.id }}')"
|
|
class="block w-full text-center py-2 px-4 rounded-lg bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-semibold transition">
|
|
{{ _("subscription.keep_plan_prefix") }} {{ t.name }}
|
|
</button>
|
|
{% elif is_upgrade %}
|
|
{% if t.id == 'business' %}
|
|
<a href="mailto:sales@docuelevate.io?subject=Business+Plan+Enquiry"
|
|
class="block text-center py-2 px-4 rounded-lg bg-gray-800 hover:bg-gray-700 text-white text-sm font-semibold transition">
|
|
Contact Sales
|
|
</a>
|
|
{% else %}
|
|
<button type="button"
|
|
onclick="changePlan('{{ t.id }}')"
|
|
class="block w-full text-center py-2 px-4 rounded-lg bg-green-600 hover:bg-green-700 text-white text-sm font-semibold transition">
|
|
{{ _("subscription.upgrade_to_prefix") }} {{ t.name }}
|
|
</button>
|
|
{% endif %}
|
|
{% elif is_pending %}
|
|
<button type="button"
|
|
onclick="cancelPendingChange()"
|
|
class="block w-full text-center py-2 px-4 rounded-lg bg-amber-100 hover:bg-amber-200 text-amber-800 text-sm font-semibold transition">
|
|
{{ _("subscription.cancel_scheduled") }}
|
|
</button>
|
|
{% else %}
|
|
<!-- Downgrade -->
|
|
<button type="button"
|
|
onclick="changePlan('{{ t.id }}')"
|
|
class="block w-full text-center py-2 px-4 rounded-lg bg-gray-100 hover:bg-gray-200 text-gray-700 text-sm font-semibold transition">
|
|
{{ _("subscription.downgrade_to_prefix") }} {{ t.name }}
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
<p class="text-xs text-gray-400 mt-3">
|
|
<i class="fas fa-info-circle mr-1" aria-hidden="true"></i>
|
|
{{ _("subscription.upgrade_info") }}
|
|
</p>
|
|
</div>
|
|
|
|
{% endif %}
|
|
|
|
<!-- Back link -->
|
|
<div class="mt-8">
|
|
<a href="/" class="text-sm text-indigo-600 hover:text-indigo-800 font-medium flex items-center gap-1">
|
|
<i class="fas fa-arrow-left text-xs" aria-hidden="true"></i> {{ _("subscription.back_to_dashboard") }}
|
|
</a>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<script>
|
|
const CSRF_TOKEN = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
|
|
|
|
function showFlash(message, type) {
|
|
const container = document.getElementById('flash-container');
|
|
let bgClass, borderClass, iconClass, textClass;
|
|
if (type === 'success') {
|
|
bgClass = 'bg-green-50'; borderClass = 'border-green-300';
|
|
iconClass = 'fa-check-circle text-green-500'; textClass = 'text-green-800';
|
|
} else if (type === 'warning') {
|
|
bgClass = 'bg-amber-50'; borderClass = 'border-amber-300';
|
|
iconClass = 'fa-clock text-amber-500'; textClass = 'text-amber-800';
|
|
} else {
|
|
bgClass = 'bg-red-50'; borderClass = 'border-red-300';
|
|
iconClass = 'fa-exclamation-circle text-red-500'; textClass = 'text-red-800';
|
|
}
|
|
container.innerHTML = `
|
|
<div class="${bgClass} border ${borderClass} rounded-xl p-4 mb-6 flex items-start gap-3" role="alert">
|
|
<i class="fas ${iconClass} text-xl flex-shrink-0 mt-0.5" aria-hidden="true"></i>
|
|
<p class="${textClass} text-sm">${message}</p>
|
|
</div>`;
|
|
container.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
}
|
|
|
|
async function changePlan(planId) {
|
|
try {
|
|
const res = await fetch('/api/subscriptions/change', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-Token': CSRF_TOKEN,
|
|
},
|
|
body: JSON.stringify({ plan_id: planId, billing_cycle: 'monthly' }),
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) {
|
|
showFlash(data.detail || 'An error occurred. Please try again.', 'error');
|
|
return;
|
|
}
|
|
const msgType = data.immediate ? 'success' : 'warning';
|
|
showFlash(data.message, msgType);
|
|
// Reload after a short delay to reflect the change
|
|
setTimeout(() => window.location.reload(), 1800);
|
|
} catch (err) {
|
|
showFlash('Network error. Please try again.', 'error');
|
|
}
|
|
}
|
|
|
|
async function cancelPendingChange() {
|
|
try {
|
|
const res = await fetch('/api/subscriptions/change', {
|
|
method: 'DELETE',
|
|
headers: { 'X-CSRF-Token': CSRF_TOKEN },
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) {
|
|
showFlash(data.detail || 'An error occurred. Please try again.', 'error');
|
|
return;
|
|
}
|
|
showFlash(data.message, 'success');
|
|
setTimeout(() => window.location.reload(), 1800);
|
|
} catch (err) {
|
|
showFlash('Network error. Please try again.', 'error');
|
|
}
|
|
}
|
|
</script>
|
|
{% endblock %}
|