update: plans
This commit is contained in:
parent
2d0f1ce302
commit
d159d1b17a
|
|
@ -1,4 +1,4 @@
|
||||||
export type Plan = "free" | "pro" | "pro4x" | "lifetime";
|
export type Plan = "free" | "pro" | "pro2x" | "pro4x" | "lifetime";
|
||||||
|
|
||||||
export interface PlanLimits {
|
export interface PlanLimits {
|
||||||
maxMonitors: number;
|
maxMonitors: number;
|
||||||
|
|
@ -6,29 +6,31 @@ export interface PlanLimits {
|
||||||
maxRegions: number;
|
maxRegions: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Base pro limits — multiplied for 2x/4x tiers
|
||||||
|
const PRO_BASE = { maxMonitors: 200, minIntervalS: 2, maxRegions: 99 };
|
||||||
|
|
||||||
const PLANS: Record<Plan, PlanLimits> = {
|
const PLANS: Record<Plan, PlanLimits> = {
|
||||||
free: {
|
free: { maxMonitors: 10, minIntervalS: 30, maxRegions: 1 },
|
||||||
maxMonitors: 10,
|
pro: { ...PRO_BASE },
|
||||||
minIntervalS: 30,
|
pro2x: { ...PRO_BASE, maxMonitors: PRO_BASE.maxMonitors * 2 },
|
||||||
maxRegions: 1,
|
pro4x: { ...PRO_BASE, maxMonitors: PRO_BASE.maxMonitors * 4 },
|
||||||
},
|
lifetime: { ...PRO_BASE },
|
||||||
pro: {
|
|
||||||
maxMonitors: 200,
|
|
||||||
minIntervalS: 2,
|
|
||||||
maxRegions: 99,
|
|
||||||
},
|
|
||||||
pro4x: {
|
|
||||||
maxMonitors: 800,
|
|
||||||
minIntervalS: 2,
|
|
||||||
maxRegions: 99,
|
|
||||||
},
|
|
||||||
lifetime: {
|
|
||||||
maxMonitors: 200,
|
|
||||||
minIntervalS: 2,
|
|
||||||
maxRegions: 99,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getPlanLimits(plan: string): PlanLimits {
|
export function getPlanLimits(plan: string): PlanLimits {
|
||||||
return PLANS[plan as Plan] || PLANS.free;
|
return PLANS[plan as Plan] || PLANS.free;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Display helpers
|
||||||
|
export const PLAN_LABELS: Record<string, string> = {
|
||||||
|
free: "Free", pro: "Pro", pro2x: "Pro 2x", pro4x: "Pro 4x", lifetime: "Lifetime",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PRO_MULTIPLIERS = [
|
||||||
|
{ plan: "pro", label: "1x", monitors: PRO_BASE.maxMonitors, priceMultiplier: 1 },
|
||||||
|
{ plan: "pro2x", label: "2x", monitors: PRO_BASE.maxMonitors * 2, priceMultiplier: 2 },
|
||||||
|
{ plan: "pro4x", label: "4x", monitors: PRO_BASE.maxMonitors * 4, priceMultiplier: 4 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const PRO_MONTHLY_USD = 12;
|
||||||
|
export const LIFETIME_USD = 140;
|
||||||
|
|
|
||||||
|
|
@ -231,7 +231,7 @@ async function applyPlan(payment: any) {
|
||||||
export async function expireProPlans() {
|
export async function expireProPlans() {
|
||||||
const result = await sql`
|
const result = await sql`
|
||||||
UPDATE accounts SET plan = 'free', plan_expires_at = NULL
|
UPDATE accounts SET plan = 'free', plan_expires_at = NULL
|
||||||
WHERE plan = 'pro' AND plan_expires_at IS NOT NULL AND plan_expires_at < now()
|
WHERE plan IN ('pro', 'pro2x', 'pro4x') AND plan_expires_at IS NOT NULL AND plan_expires_at < now()
|
||||||
`;
|
`;
|
||||||
if (result.count > 0) console.log(`Downgraded ${result.count} expired pro accounts`);
|
if (result.count > 0) console.log(`Downgraded ${result.count} expired pro accounts`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,13 @@
|
||||||
export const PLANS = {
|
// Pro pricing — base $12/mo, multiplied for 2x/4x
|
||||||
pro: {
|
export const PRO_MONTHLY_USD = 12;
|
||||||
label: "Pro",
|
export const LIFETIME_USD = 140;
|
||||||
monthlyUsd: 12,
|
|
||||||
},
|
export const PLANS: Record<string, { label: string; monthlyUsd?: number; priceUsd?: number }> = {
|
||||||
pro4x: {
|
pro: { label: "Pro", monthlyUsd: PRO_MONTHLY_USD },
|
||||||
label: "Pro 4x",
|
pro2x: { label: "Pro 2x", monthlyUsd: PRO_MONTHLY_USD * 2 },
|
||||||
monthlyUsd: 48,
|
pro4x: { label: "Pro 4x", monthlyUsd: PRO_MONTHLY_USD * 4 },
|
||||||
},
|
lifetime: { label: "Lifetime", priceUsd: LIFETIME_USD },
|
||||||
lifetime: {
|
};
|
||||||
label: "Lifetime",
|
|
||||||
priceUsd: 140,
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const COINS: Record<string, { label: string; ticker: string; confirmations: number; uri: string }> = {
|
export const COINS: Record<string, { label: string; ticker: string; confirmations: number; uri: string }> = {
|
||||||
btc: { label: "Bitcoin", ticker: "BTC", confirmations: 1, uri: "bitcoin" },
|
btc: { label: "Bitcoin", ticker: "BTC", confirmations: 1, uri: "bitcoin" },
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,11 @@ export async function generateReceipt(paymentId: number): Promise<string> {
|
||||||
? new Date(payment.paid_at).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" })
|
? new Date(payment.paid_at).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" })
|
||||||
: "—";
|
: "—";
|
||||||
const createdDate = new Date(payment.created_at).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" });
|
const createdDate = new Date(payment.created_at).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" });
|
||||||
|
const planNames: Record<string, string> = { pro: "Pro", pro2x: "Pro 2x", pro4x: "Pro 4x", lifetime: "Lifetime" };
|
||||||
|
const planName = planNames[payment.plan] || payment.plan;
|
||||||
const planLabel = payment.plan === "lifetime"
|
const planLabel = payment.plan === "lifetime"
|
||||||
? "Lifetime"
|
? "Lifetime"
|
||||||
: `Pro × ${payment.months} month${payment.months > 1 ? "s" : ""}`;
|
: `${planName} × ${payment.months} month${payment.months > 1 ? "s" : ""}`;
|
||||||
|
|
||||||
const txRows = txs.map((tx: any) => {
|
const txRows = txs.map((tx: any) => {
|
||||||
const date = new Date(tx.detected_at).toLocaleDateString("en-US", {
|
const date = new Date(tx.detected_at).toLocaleDateString("en-US", {
|
||||||
|
|
|
||||||
|
|
@ -72,8 +72,9 @@ export const routes = new Elysia()
|
||||||
if (!available.includes(coin)) { set.status = 400; return { error: `${coin} is temporarily unavailable` }; }
|
if (!available.includes(coin)) { set.status = 400; return { error: `${coin} is temporarily unavailable` }; }
|
||||||
|
|
||||||
// Calculate amount
|
// Calculate amount
|
||||||
const planPricing = plan === "lifetime" ? null : (plan === "pro4x" ? PLANS.pro4x : PLANS.pro);
|
const planDef = PLANS[plan];
|
||||||
const amountUsd = plan === "lifetime" ? PLANS.lifetime.priceUsd : planPricing!.monthlyUsd * (months ?? 1);
|
if (!planDef) { set.status = 400; return { error: `Unknown plan: ${plan}` }; }
|
||||||
|
const amountUsd = planDef.priceUsd ?? (planDef.monthlyUsd! * (months ?? 1));
|
||||||
const rates = await getExchangeRates();
|
const rates = await getExchangeRates();
|
||||||
const rate = rates[coin];
|
const rate = rates[coin];
|
||||||
if (!rate) { set.status = 500; return { error: "Could not fetch exchange rate" }; }
|
if (!rate) { set.status = 500; return { error: "Could not fetch exchange rate" }; }
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,15 @@
|
||||||
%>
|
%>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Show months section when pro or pro4x is selected */
|
/* Show pro options when pro is selected */
|
||||||
|
#plan-pro:checked ~ #pro-options { display: block; }
|
||||||
|
/* Show months for pro (not lifetime) */
|
||||||
#plan-pro:checked ~ #months-section { display: block; }
|
#plan-pro:checked ~ #months-section { display: block; }
|
||||||
#plan-pro4x:checked ~ #months-section { display: block; }
|
|
||||||
/* Highlight selected plan */
|
/* Highlight selected plan */
|
||||||
#plan-pro:checked ~ .plan-labels label[for="plan-pro"] { border-color: rgb(59 130 246 / 0.5); }
|
#plan-pro:checked ~ .plan-labels label[for="plan-pro"] { border-color: rgb(59 130 246 / 0.5); }
|
||||||
#plan-pro4x:checked ~ .plan-labels label[for="plan-pro4x"] { border-color: rgb(59 130 246 / 0.5); }
|
|
||||||
#plan-lifetime:checked ~ .plan-labels label[for="plan-lifetime"] { border-color: rgb(234 179 8 / 0.5); }
|
#plan-lifetime:checked ~ .plan-labels label[for="plan-lifetime"] { border-color: rgb(234 179 8 / 0.5); }
|
||||||
|
/* Highlight selected tier */
|
||||||
|
.tier-radio:checked + label { border-color: rgb(59 130 246 / 0.5); }
|
||||||
/* Highlight selected coin */
|
/* Highlight selected coin */
|
||||||
.coin-radio:checked + label { border-color: rgb(59 130 246); }
|
.coin-radio:checked + label { border-color: rgb(59 130 246); }
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -30,43 +32,64 @@
|
||||||
<!-- ─── Step 1: Plan & coin selection (form, works without JS) ─── -->
|
<!-- ─── Step 1: Plan & coin selection (form, works without JS) ─── -->
|
||||||
<form action="/dashboard/checkout" method="POST" class="space-y-6">
|
<form action="/dashboard/checkout" method="POST" class="space-y-6">
|
||||||
|
|
||||||
<!-- Hidden radios for plan (CSS uses :checked) -->
|
<!-- Hidden radios for plan type (CSS uses :checked) -->
|
||||||
<input type="radio" name="plan" value="pro" id="plan-pro" class="hidden peer/pro" checked>
|
<input type="radio" name="planType" value="pro" id="plan-pro" class="hidden" checked>
|
||||||
<input type="radio" name="plan" value="pro4x" id="plan-pro4x" class="hidden peer/pro4x">
|
<input type="radio" name="planType" value="lifetime" id="plan-lifetime" class="hidden">
|
||||||
<input type="radio" name="plan" value="lifetime" id="plan-lifetime" class="hidden peer/lifetime">
|
|
||||||
|
|
||||||
<div class="plan-labels grid grid-cols-3 gap-4">
|
<div class="plan-labels grid grid-cols-2 gap-4">
|
||||||
<label for="plan-pro"
|
<label for="plan-pro"
|
||||||
class="cursor-pointer text-left bg-surface border-2 border-border-subtle hover:border-blue-500/40 rounded-xl p-5 transition-colors">
|
class="cursor-pointer text-left bg-surface border-2 border-border-subtle hover:border-blue-500/40 rounded-xl p-5 transition-colors">
|
||||||
<div class="text-xs text-gray-500 uppercase tracking-wider font-mono mb-1">Pro</div>
|
<div class="text-xs text-gray-500 uppercase tracking-wider font-mono mb-1">Pro</div>
|
||||||
<div class="text-2xl font-bold text-gray-100">$12<span class="text-sm font-normal text-gray-500">/mo</span></div>
|
<div class="text-2xl font-bold text-gray-100">From $12<span class="text-sm font-normal text-gray-500">/mo</span></div>
|
||||||
<div class="text-xs text-gray-500 mt-2">200 monitors, 2s intervals</div>
|
<div class="text-xs text-gray-500 mt-2">200–800 monitors, 2s intervals</div>
|
||||||
</label>
|
|
||||||
<label for="plan-pro4x"
|
|
||||||
class="cursor-pointer text-left bg-surface border-2 border-border-subtle hover:border-blue-500/40 rounded-xl p-5 transition-colors">
|
|
||||||
<div class="text-xs text-gray-500 uppercase tracking-wider font-mono mb-1">Pro 4x</div>
|
|
||||||
<div class="text-2xl font-bold text-gray-100">$48<span class="text-sm font-normal text-gray-500">/mo</span></div>
|
|
||||||
<div class="text-xs text-gray-500 mt-2">800 monitors, 2s intervals</div>
|
|
||||||
</label>
|
</label>
|
||||||
<label for="plan-lifetime"
|
<label for="plan-lifetime"
|
||||||
class="cursor-pointer text-left bg-surface border-2 border-border-subtle hover:border-yellow-500/40 rounded-xl p-5 transition-colors relative">
|
class="cursor-pointer text-left bg-surface border-2 border-border-subtle hover:border-yellow-500/40 rounded-xl p-5 transition-colors relative">
|
||||||
<span class="absolute top-3 right-3 text-[10px] font-semibold px-2 py-0.5 rounded-full bg-yellow-500/15 text-yellow-500 border border-yellow-500/20">Launch Deal</span>
|
<span class="absolute top-3 right-3 text-[10px] font-semibold px-2 py-0.5 rounded-full bg-yellow-500/15 text-yellow-500 border border-yellow-500/20">Launch Deal</span>
|
||||||
<div class="text-xs text-yellow-500/70 uppercase tracking-wider font-mono mb-1">Lifetime</div>
|
<div class="text-xs text-yellow-500/70 uppercase tracking-wider font-mono mb-1">Lifetime</div>
|
||||||
<div class="text-2xl font-bold text-gray-100">$140</div>
|
<div class="text-2xl font-bold text-gray-100">$140</div>
|
||||||
<div class="text-xs text-gray-500 mt-2">One-time, forever</div>
|
<div class="text-xs text-gray-500 mt-2">One-time, 200 monitors forever</div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Months (visible only when pro/pro4x selected, via CSS) -->
|
<!-- Pro tier selection (visible only when pro selected) -->
|
||||||
<div id="months-section" class="hidden">
|
<div id="pro-options" class="hidden space-y-4">
|
||||||
<label class="block text-sm text-gray-400 mb-2">How many months?</label>
|
<!-- Tier selector -->
|
||||||
<select id="months-select" name="months" class="input-base px-4 py-2.5 text-gray-100">
|
<div>
|
||||||
<% for (let i = 1; i <= 12; i++) { %>
|
<label class="block text-sm text-gray-400 mb-2">Tier</label>
|
||||||
<option value="<%= i %>" data-pro="$<%= i * 12 %>" data-pro4x="$<%= i * 48 %>"><%= i %> month<%= i > 1 ? 's' : '' %> — $<%= i * 12 %></option>
|
<div class="grid grid-cols-3 gap-2">
|
||||||
<% } %>
|
<input type="radio" name="plan" value="pro" id="tier-1x" class="hidden tier-radio" checked>
|
||||||
</select>
|
<label for="tier-1x" class="cursor-pointer text-center bg-surface border-2 border-border-subtle hover:border-blue-500/40 rounded-lg py-2.5 transition-colors">
|
||||||
|
<div class="text-sm font-semibold text-gray-200">1x</div>
|
||||||
|
<div class="text-xs text-gray-500">200 monitors · $12/mo</div>
|
||||||
|
</label>
|
||||||
|
<input type="radio" name="plan" value="pro2x" id="tier-2x" class="hidden tier-radio">
|
||||||
|
<label for="tier-2x" class="cursor-pointer text-center bg-surface border-2 border-border-subtle hover:border-blue-500/40 rounded-lg py-2.5 transition-colors">
|
||||||
|
<div class="text-sm font-semibold text-gray-200">2x</div>
|
||||||
|
<div class="text-xs text-gray-500">400 monitors · $24/mo</div>
|
||||||
|
</label>
|
||||||
|
<input type="radio" name="plan" value="pro4x" id="tier-4x" class="hidden tier-radio">
|
||||||
|
<label for="tier-4x" class="cursor-pointer text-center bg-surface border-2 border-border-subtle hover:border-blue-500/40 rounded-lg py-2.5 transition-colors">
|
||||||
|
<div class="text-sm font-semibold text-gray-200">4x</div>
|
||||||
|
<div class="text-xs text-gray-500">800 monitors · $48/mo</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Months -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm text-gray-400 mb-2">How many months?</label>
|
||||||
|
<select id="months-select" name="months" class="input-base px-4 py-2.5 text-gray-100">
|
||||||
|
<% for (let i = 1; i <= 12; i++) { %>
|
||||||
|
<option value="<%= i %>" data-base="<%= i * 12 %>"><%= i %> month<%= i > 1 ? 's' : '' %> — $<%= i * 12 %></option>
|
||||||
|
<% } %>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Hidden plan field for lifetime (JS sets this) -->
|
||||||
|
<input type="hidden" name="plan" id="plan-value" value="pro">
|
||||||
|
|
||||||
<!-- Coin selection -->
|
<!-- Coin selection -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm text-gray-400 mb-2">Pay with</label>
|
<label class="block text-sm text-gray-400 mb-2">Pay with</label>
|
||||||
|
|
@ -204,18 +227,38 @@
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Update month pricing when switching between pro and pro4x
|
const multipliers = { pro: 1, pro2x: 2, pro4x: 4 };
|
||||||
document.querySelectorAll('input[name="plan"]').forEach(radio => {
|
const planValue = document.getElementById('plan-value');
|
||||||
|
|
||||||
|
// Plan type toggle (pro vs lifetime)
|
||||||
|
document.querySelectorAll('input[name="planType"]').forEach(radio => {
|
||||||
radio.addEventListener('change', () => {
|
radio.addEventListener('change', () => {
|
||||||
const sel = document.getElementById('months-select');
|
if (radio.value === 'lifetime') {
|
||||||
if (!sel) return;
|
planValue.value = 'lifetime';
|
||||||
const plan = radio.value;
|
} else {
|
||||||
for (const opt of sel.options) {
|
const tier = document.querySelector('.tier-radio:checked');
|
||||||
const price = opt.dataset[plan] || opt.dataset.pro;
|
planValue.value = tier ? tier.value : 'pro';
|
||||||
if (price) opt.textContent = opt.value + ' month' + (opt.value > 1 ? 's' : '') + ' — ' + price;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Tier toggle (1x/2x/4x)
|
||||||
|
document.querySelectorAll('.tier-radio').forEach(radio => {
|
||||||
|
radio.addEventListener('change', () => {
|
||||||
|
planValue.value = radio.value;
|
||||||
|
updateMonthPricing(radio.value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateMonthPricing(plan) {
|
||||||
|
const sel = document.getElementById('months-select');
|
||||||
|
if (!sel) return;
|
||||||
|
const mult = multipliers[plan] || 1;
|
||||||
|
for (const opt of sel.options) {
|
||||||
|
const base = parseInt(opt.dataset.base);
|
||||||
|
opt.textContent = opt.value + ' month' + (opt.value > 1 ? 's' : '') + ' — $' + (base * mult);
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<%~ include('./partials/foot') %>
|
<%~ include('./partials/foot') %>
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,14 @@
|
||||||
const hasEmail = !!it.account.email_hash;
|
const hasEmail = !!it.account.email_hash;
|
||||||
const createdDate = new Date(it.account.created_at).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
|
const createdDate = new Date(it.account.created_at).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
|
||||||
const plan = it.account.plan || 'free';
|
const plan = it.account.plan || 'free';
|
||||||
const planLabel = { free: 'Free', pro: 'Pro', pro4x: 'Pro 4x', lifetime: 'Lifetime' }[plan] || plan;
|
const planLabel = { free: 'Free', pro: 'Pro', pro2x: 'Pro 2x', pro4x: 'Pro 4x', lifetime: 'Lifetime' }[plan] || plan;
|
||||||
const limits = { free: { monitors: 10, interval: '30s', regions: 1 }, pro: { monitors: 200, interval: '2s', regions: 'All' }, pro4x: { monitors: 800, interval: '2s', regions: 'All' }, lifetime: { monitors: 200, interval: '2s', regions: 'All' } }[plan] || { monitors: 10, interval: '30s', regions: 1 };
|
const limits = {
|
||||||
|
free: { monitors: 10, interval: '30s', regions: 1 },
|
||||||
|
pro: { monitors: 200, interval: '2s', regions: 'All' },
|
||||||
|
pro2x: { monitors: 400, interval: '2s', regions: 'All' },
|
||||||
|
pro4x: { monitors: 800, interval: '2s', regions: 'All' },
|
||||||
|
lifetime: { monitors: 200, interval: '2s', regions: 'All' },
|
||||||
|
}[plan] || { monitors: 10, interval: '30s', regions: 1 };
|
||||||
%>
|
%>
|
||||||
|
|
||||||
<main class="max-w-3xl mx-auto px-8 py-10 space-y-8">
|
<main class="max-w-3xl mx-auto px-8 py-10 space-y-8">
|
||||||
|
|
@ -19,7 +25,7 @@
|
||||||
<h2 class="text-sm font-semibold text-gray-300">Plan</h2>
|
<h2 class="text-sm font-semibold text-gray-300">Plan</h2>
|
||||||
<% if (plan === 'free') { %>
|
<% if (plan === 'free') { %>
|
||||||
<span class="text-xs font-medium px-2.5 py-1 rounded-full bg-gray-800/50 border border-border-subtle text-gray-400">Free</span>
|
<span class="text-xs font-medium px-2.5 py-1 rounded-full bg-gray-800/50 border border-border-subtle text-gray-400">Free</span>
|
||||||
<% } else if (plan === 'pro' || plan === 'pro4x') { %>
|
<% } else if (plan === 'pro' || plan === 'pro2x' || plan === 'pro4x') { %>
|
||||||
<span class="text-xs font-medium px-2.5 py-1 rounded-full bg-gradient-to-r from-blue-600/20 to-blue-500/10 border border-blue-500/30 text-blue-400"><%= planLabel %></span>
|
<span class="text-xs font-medium px-2.5 py-1 rounded-full bg-gradient-to-r from-blue-600/20 to-blue-500/10 border border-blue-500/30 text-blue-400"><%= planLabel %></span>
|
||||||
<% } else if (plan === 'lifetime') { %>
|
<% } else if (plan === 'lifetime') { %>
|
||||||
<span class="text-xs font-medium px-2.5 py-1 rounded-full bg-gradient-to-r from-yellow-600/20 to-yellow-500/10 border border-yellow-500/30 text-yellow-400">Lifetime</span>
|
<span class="text-xs font-medium px-2.5 py-1 rounded-full bg-gradient-to-r from-yellow-600/20 to-yellow-500/10 border border-yellow-500/30 text-yellow-400">Lifetime</span>
|
||||||
|
|
@ -43,7 +49,7 @@
|
||||||
<div class="mt-4 pt-4 border-t divider">
|
<div class="mt-4 pt-4 border-t divider">
|
||||||
<a href="/dashboard/checkout" class="btn-primary inline-flex items-center gap-2 px-4 py-2 text-sm">Upgrade to Pro</a>
|
<a href="/dashboard/checkout" class="btn-primary inline-flex items-center gap-2 px-4 py-2 text-sm">Upgrade to Pro</a>
|
||||||
</div>
|
</div>
|
||||||
<% } else if ((plan === 'pro' || plan === 'pro4x') && it.account.plan_expires_at) { %>
|
<% } else if ((plan === 'pro' || plan === 'pro2x' || plan === 'pro4x') && it.account.plan_expires_at) { %>
|
||||||
<div class="mt-4 pt-4 border-t divider text-xs text-gray-500">
|
<div class="mt-4 pt-4 border-t divider text-xs text-gray-500">
|
||||||
Pro plan expires <%= new Date(it.account.plan_expires_at).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) %>.
|
Pro plan expires <%= new Date(it.account.plan_expires_at).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) %>.
|
||||||
<a href="/dashboard/checkout" class="text-blue-400 hover:text-blue-300 ml-1">Extend or upgrade to Lifetime</a>
|
<a href="/dashboard/checkout" class="text-blue-400 hover:text-blue-300 ml-1">Extend or upgrade to Lifetime</a>
|
||||||
|
|
@ -160,7 +166,8 @@
|
||||||
const statusColors = { paid: 'green', confirming: 'blue', pending: 'yellow', underpaid: 'orange' };
|
const statusColors = { paid: 'green', confirming: 'blue', pending: 'yellow', underpaid: 'orange' };
|
||||||
const statusColor = statusColors[inv.status] || 'gray';
|
const statusColor = statusColors[inv.status] || 'gray';
|
||||||
const date = new Date(inv.created_at).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
|
const date = new Date(inv.created_at).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
|
||||||
const planLabel = inv.plan === 'lifetime' ? 'Lifetime' : `Pro × ${inv.months}mo`;
|
const invPlanNames = { pro: 'Pro', pro2x: 'Pro 2x', pro4x: 'Pro 4x', lifetime: 'Lifetime' };
|
||||||
|
const planLabel = inv.plan === 'lifetime' ? 'Lifetime' : `${invPlanNames[inv.plan] || 'Pro'} × ${inv.months}mo`;
|
||||||
%>
|
%>
|
||||||
<div class="flex items-center justify-between p-3 rounded-lg bg-surface border border-border-subtle hover:border-border-strong transition-colors">
|
<div class="flex items-center justify-between p-3 rounded-lg bg-surface border border-border-subtle hover:border-border-strong transition-colors">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue