add: 4x pro plan

This commit is contained in:
nate 2026-03-22 05:33:39 +04:00
parent 014976027b
commit 1049677f19
7 changed files with 86 additions and 34 deletions

View File

@ -1,4 +1,4 @@
export type Plan = "free" | "pro" | "lifetime"; export type Plan = "free" | "pro" | "pro4x" | "lifetime";
export interface PlanLimits { export interface PlanLimits {
maxMonitors: number; maxMonitors: number;
@ -17,6 +17,11 @@ const PLANS: Record<Plan, PlanLimits> = {
minIntervalS: 2, minIntervalS: 2,
maxRegions: 99, maxRegions: 99,
}, },
pro4x: {
maxMonitors: 800,
minIntervalS: 2,
maxRegions: 99,
},
lifetime: { lifetime: {
maxMonitors: 200, maxMonitors: 200,
minIntervalS: 2, minIntervalS: 2,

View File

@ -219,10 +219,11 @@ async function applyPlan(payment: any) {
const [acc] = await sql`SELECT plan, plan_expires_at FROM accounts WHERE id = ${payment.account_id}`; const [acc] = await sql`SELECT plan, plan_expires_at FROM accounts WHERE id = ${payment.account_id}`;
const now = new Date(); const now = new Date();
const expiry = acc.plan_expires_at ? new Date(acc.plan_expires_at) : null; const expiry = acc.plan_expires_at ? new Date(acc.plan_expires_at) : null;
const base = (acc.plan === "pro" && expiry && expiry > now) ? expiry : now; const samePlan = acc.plan === payment.plan && expiry && expiry > now;
const base = samePlan ? expiry : now;
const newExpiry = new Date(base); const newExpiry = new Date(base);
newExpiry.setMonth(newExpiry.getMonth() + payment.months); newExpiry.setMonth(newExpiry.getMonth() + payment.months);
await sql`UPDATE accounts SET plan = 'pro', plan_expires_at = ${newExpiry.toISOString()} WHERE id = ${payment.account_id}`; await sql`UPDATE accounts SET plan = ${payment.plan}, plan_expires_at = ${newExpiry.toISOString()} WHERE id = ${payment.account_id}`;
} }
console.log(`Payment ${payment.id} activated: ${payment.plan} for account ${payment.account_id}`); console.log(`Payment ${payment.id} activated: ${payment.plan} for account ${payment.account_id}`);
} }

View File

@ -3,6 +3,10 @@ export const PLANS = {
label: "Pro", label: "Pro",
monthlyUsd: 12, monthlyUsd: 12,
}, },
pro4x: {
label: "Pro 4x",
monthlyUsd: 48,
},
lifetime: { lifetime: {
label: "Lifetime", label: "Lifetime",
priceUsd: 140, priceUsd: 140,

View File

@ -72,7 +72,8 @@ 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 amountUsd = plan === "lifetime" ? PLANS.lifetime.priceUsd : PLANS.pro.monthlyUsd * (months ?? 1); const planPricing = plan === "lifetime" ? null : (plan === "pro4x" ? PLANS.pro4x : PLANS.pro);
const amountUsd = plan === "lifetime" ? PLANS.lifetime.priceUsd : planPricing!.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" }; }

View File

@ -9,10 +9,12 @@
%> %>
<style> <style>
/* Show months section only when pro is selected */ /* Show months section when pro or pro4x is selected */
#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 coin */ /* Highlight selected coin */
.coin-radio:checked + label { border-color: rgb(59 130 246); } .coin-radio:checked + label { border-color: rgb(59 130 246); }
@ -30,15 +32,22 @@
<!-- Hidden radios for plan (CSS uses :checked) --> <!-- Hidden radios for plan (CSS uses :checked) -->
<input type="radio" name="plan" value="pro" id="plan-pro" class="hidden peer/pro" checked> <input type="radio" name="plan" value="pro" id="plan-pro" class="hidden peer/pro" checked>
<input type="radio" name="plan" value="pro4x" id="plan-pro4x" class="hidden peer/pro4x">
<input type="radio" name="plan" value="lifetime" id="plan-lifetime" class="hidden peer/lifetime"> <input type="radio" name="plan" value="lifetime" id="plan-lifetime" class="hidden peer/lifetime">
<div class="plan-labels grid grid-cols-2 gap-4"> <div class="plan-labels grid grid-cols-3 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">$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 monitors, 2s intervals</div>
</label> </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 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>
@ -48,12 +57,12 @@
</label> </label>
</div> </div>
<!-- Months (visible only when pro selected, via CSS) --> <!-- Months (visible only when pro/pro4x selected, via CSS) -->
<div id="months-section" class="hidden"> <div id="months-section" class="hidden">
<label class="block text-sm text-gray-400 mb-2">How many months?</label> <label class="block text-sm text-gray-400 mb-2">How many months?</label>
<select name="months" class="input-base px-4 py-2.5 text-gray-100"> <select id="months-select" name="months" class="input-base px-4 py-2.5 text-gray-100">
<% for (let i = 1; i <= 12; i++) { %> <% for (let i = 1; i <= 12; i++) { %>
<option value="<%= i %>"><%= i %> month<%= i > 1 ? 's' : '' %> — $<%= i * 12 %></option> <option value="<%= i %>" data-pro="$<%= i * 12 %>" data-pro4x="$<%= i * 48 %>"><%= i %> month<%= i > 1 ? 's' : '' %> — $<%= i * 12 %></option>
<% } %> <% } %>
</select> </select>
</div> </div>
@ -194,4 +203,19 @@
</main> </main>
<script>
// Update month pricing when switching between pro and pro4x
document.querySelectorAll('input[name="plan"]').forEach(radio => {
radio.addEventListener('change', () => {
const sel = document.getElementById('months-select');
if (!sel) return;
const plan = radio.value;
for (const opt of sel.options) {
const price = opt.dataset[plan] || opt.dataset.pro;
if (price) opt.textContent = opt.value + ' month' + (opt.value > 1 ? 's' : '') + ' — ' + price;
}
});
});
</script>
<%~ include('./partials/foot') %> <%~ include('./partials/foot') %>

View File

@ -482,13 +482,13 @@
<h2 class="text-3xl sm:text-4xl font-bold tracking-tight mb-4">Simple pricing</h2> <h2 class="text-3xl sm:text-4xl font-bold tracking-tight mb-4">Simple pricing</h2>
<p class="text-gray-400 text-lg mb-12">Start for free. No credit card required.</p> <p class="text-gray-400 text-lg mb-12">Start for free. No credit card required.</p>
<div class="grid sm:grid-cols-3 gap-6 max-w-4xl mx-auto"> <div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-5 max-w-5xl mx-auto">
<!-- Free --> <!-- Free -->
<div class="pricing-free rounded-xl p-8 text-left"> <div class="pricing-free rounded-xl p-7 text-left">
<div class="text-xs text-gray-500 uppercase tracking-wider font-mono mb-2">Free</div> <div class="text-xs text-gray-500 uppercase tracking-wider font-mono mb-2">Free</div>
<div class="text-4xl font-bold mb-1">$0</div> <div class="text-3xl font-bold mb-1">$0</div>
<div class="text-sm text-gray-500 mb-6">forever</div> <div class="text-sm text-gray-500 mb-5">forever</div>
<ul class="space-y-3 text-sm text-gray-400"> <ul class="space-y-2.5 text-sm text-gray-400">
<li class="flex items-center gap-2"> <li class="flex items-center gap-2">
<svg class="w-4 h-4 text-green-400 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg> <svg class="w-4 h-4 text-green-400 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
10 monitors 10 monitors
@ -499,21 +499,17 @@
</li> </li>
<li class="flex items-center gap-2"> <li class="flex items-center gap-2">
<svg class="w-4 h-4 text-green-400 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg> <svg class="w-4 h-4 text-green-400 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
Full query language Single region
</li>
<li class="flex items-center gap-2">
<svg class="w-4 h-4 text-green-400 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
Single region monitors
</li> </li>
</ul> </ul>
</div> </div>
<!-- Pro --> <!-- Pro -->
<div class="pricing-pro rounded-xl p-8 text-left relative"> <div class="pricing-pro rounded-xl p-7 text-left">
<div class="text-xs text-blue-400 uppercase tracking-wider font-mono mb-2">Pro</div> <div class="text-xs text-blue-400 uppercase tracking-wider font-mono mb-2">Pro</div>
<div class="text-4xl font-bold mb-1">$12</div> <div class="text-3xl font-bold mb-1">$12</div>
<div class="text-sm text-gray-500 mb-6">per month</div> <div class="text-sm text-gray-500 mb-5">per month</div>
<ul class="space-y-3 text-sm text-gray-400"> <ul class="space-y-2.5 text-sm text-gray-400">
<li class="flex items-center gap-2"> <li class="flex items-center gap-2">
<svg class="w-4 h-4 text-blue-400 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg> <svg class="w-4 h-4 text-blue-400 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
200 monitors 200 monitors
@ -524,25 +520,46 @@
</li> </li>
<li class="flex items-center gap-2"> <li class="flex items-center gap-2">
<svg class="w-4 h-4 text-blue-400 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg> <svg class="w-4 h-4 text-blue-400 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
Multi-region monitors Multi-region
</li>
</ul>
</div>
<!-- Pro 4x -->
<div class="pricing-pro rounded-xl p-7 text-left">
<div class="text-xs text-blue-400 uppercase tracking-wider font-mono mb-2">Pro 4x</div>
<div class="text-3xl font-bold mb-1">$48</div>
<div class="text-sm text-gray-500 mb-5">per month</div>
<ul class="space-y-2.5 text-sm text-gray-400">
<li class="flex items-center gap-2">
<svg class="w-4 h-4 text-blue-400 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
800 monitors
</li>
<li class="flex items-center gap-2">
<svg class="w-4 h-4 text-blue-400 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
2s check interval
</li>
<li class="flex items-center gap-2">
<svg class="w-4 h-4 text-blue-400 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
Multi-region
</li> </li>
</ul> </ul>
</div> </div>
<!-- Lifetime --> <!-- Lifetime -->
<div class="pricing-lifetime rounded-xl p-8 text-left relative"> <div class="pricing-lifetime rounded-xl p-7 text-left 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 uppercase tracking-wider font-mono mb-2">Lifetime</div> <div class="text-xs text-yellow-500 uppercase tracking-wider font-mono mb-2">Lifetime</div>
<div class="text-4xl font-bold mb-1">$140</div> <div class="text-3xl font-bold mb-1">$140</div>
<div class="text-sm text-gray-500 mb-6">one-time</div> <div class="text-sm text-gray-500 mb-5">one-time</div>
<ul class="space-y-3 text-sm text-gray-400"> <ul class="space-y-2.5 text-sm text-gray-400">
<li class="flex items-center gap-2"> <li class="flex items-center gap-2">
<svg class="w-4 h-4 text-yellow-500 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg> <svg class="w-4 h-4 text-yellow-500 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
Everything in Pro Everything in Pro
</li> </li>
<li class="flex items-center gap-2"> <li class="flex items-center gap-2">
<svg class="w-4 h-4 text-yellow-500 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg> <svg class="w-4 h-4 text-yellow-500 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
Pay once, use forever Pay once, forever
</li> </li>
<li class="flex items-center gap-2"> <li class="flex items-center gap-2">
<svg class="w-4 h-4 text-yellow-500 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg> <svg class="w-4 h-4 text-yellow-500 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>

View File

@ -5,8 +5,8 @@
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', lifetime: 'Lifetime' }[plan] || plan; const planLabel = { free: 'Free', pro: 'Pro', pro4x: 'Pro 4x', lifetime: 'Lifetime' }[plan] || plan;
const limits = { free: { monitors: 10, interval: '30s', regions: 1 }, pro: { monitors: 200, 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' }, 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,8 +19,8 @@
<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') { %> <% } else if (plan === 'pro' || 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">Pro</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 +43,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' && it.account.plan_expires_at) { %> <% } else if ((plan === 'pro' || 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>