added pricing calculator

This commit is contained in:
Chris Zhu
2025-03-13 12:56:45 -07:00
parent d75a13c80a
commit adc009be6b
4 changed files with 863 additions and 6 deletions

View File

@@ -0,0 +1,297 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = [
"teamSize", "teamSizeValue",
"webCpu", "webCpuValue", "webCpuShared", "webCpuDedicated",
"webMemory", "webMemoryValue",
"webInstances", "webInstancesValue",
"webEgress", "webEgressValue",
"workerCpu", "workerCpuValue", "workerCpuShared", "workerCpuDedicated",
"workerMemory", "workerMemoryValue",
"workerInstances", "workerInstancesValue",
"provider"
]
static values = {
providers: Array
}
connect() {
// Initialize sliders and pricing on page load
this.updatePricing()
}
// Handle slider input changes
teamSizeTargetInput() {
this.teamSizeValueTarget.textContent = this.teamSizeTarget.value
this.updatePricing()
}
webCpuTargetInput() {
this.webCpuValueTarget.textContent = this.webCpuTarget.value
this.updatePricing()
}
webMemoryTargetInput() {
this.webMemoryValueTarget.textContent = this.webMemoryTarget.value
this.updatePricing()
}
webInstancesTargetInput() {
this.webInstancesValueTarget.textContent = this.webInstancesTarget.value
this.updatePricing()
}
webEgressTargetInput() {
this.webEgressValueTarget.textContent = this.webEgressTarget.value
this.updatePricing()
}
workerCpuTargetInput() {
this.workerCpuValueTarget.textContent = this.workerCpuTarget.value
this.updatePricing()
}
workerMemoryTargetInput() {
this.workerMemoryValueTarget.textContent = this.workerMemoryTarget.value
this.updatePricing()
}
workerInstancesTargetInput() {
this.workerInstancesValueTarget.textContent = this.workerInstancesTarget.value
this.updatePricing()
}
// Toggle CPU mode between shared and dedicated
toggleWebCpuMode(event) {
const isShared = event.currentTarget === this.webCpuSharedTarget
if (isShared) {
this.webCpuSharedTarget.classList.add('bg-emerald-500')
this.webCpuSharedTarget.classList.remove('bg-slate-700')
this.webCpuDedicatedTarget.classList.add('bg-slate-700')
this.webCpuDedicatedTarget.classList.remove('bg-emerald-500')
} else {
this.webCpuDedicatedTarget.classList.add('bg-emerald-500')
this.webCpuDedicatedTarget.classList.remove('bg-slate-700')
this.webCpuSharedTarget.classList.add('bg-slate-700')
this.webCpuSharedTarget.classList.remove('bg-emerald-500')
}
this.updatePricing()
}
// Toggle worker CPU mode
toggleWorkerCpuMode(event) {
const isShared = event.currentTarget === this.workerCpuSharedTarget
if (isShared) {
this.workerCpuSharedTarget.classList.add('bg-emerald-500')
this.workerCpuSharedTarget.classList.remove('bg-slate-700')
this.workerCpuDedicatedTarget.classList.add('bg-slate-700')
this.workerCpuDedicatedTarget.classList.remove('bg-emerald-500')
} else {
this.workerCpuDedicatedTarget.classList.add('bg-emerald-500')
this.workerCpuDedicatedTarget.classList.remove('bg-slate-700')
this.workerCpuSharedTarget.classList.add('bg-slate-700')
this.workerCpuSharedTarget.classList.remove('bg-emerald-500')
}
this.updatePricing()
}
// Get all configuration values
getConfig() {
return {
teamSize: parseInt(this.teamSizeTarget.value),
webCpu: parseFloat(this.webCpuTarget.value),
webMemory: parseFloat(this.webMemoryTarget.value),
webInstances: parseInt(this.webInstancesTarget.value),
webEgress: parseInt(this.webEgressTarget.value),
workerCpu: parseFloat(this.workerCpuTarget.value),
workerMemory: parseFloat(this.workerMemoryTarget.value),
workerInstances: parseInt(this.workerInstancesTarget.value),
isWebCpuShared: this.webCpuSharedTarget.classList.contains('bg-emerald-500'),
isWorkerCpuShared: this.workerCpuSharedTarget.classList.contains('bg-emerald-500')
}
}
// Calculate pricing for all providers
updatePricing() {
const config = this.getConfig()
// Calculate pricing for each provider
const results = this.calculateAllProviderPricing(config)
// Update UI with calculated prices
this.updateUI(results)
// Highlight the cheapest option
this.highlightCheapestProvider(results)
}
// Calculate pricing for all providers
calculateAllProviderPricing(config) {
return {
railway: this.calculateRailwayPricing(config),
heroku: this.calculateHerokuPricing(config),
fly: this.calculateFlyPricing(config),
render: this.calculateRenderPricing(config)
}
}
// Calculate Railway pricing
calculateRailwayPricing(config) {
const cpuCost = config.webCpu * (config.isWebCpuShared ? 20 : 30) * config.webInstances
const memoryCost = config.webMemory * 20 * config.webInstances
const egressCost = config.webEgress * 0.5
const workerCpuCost = config.workerCpu * (config.isWorkerCpuShared ? 20 : 30) * config.workerInstances
const workerMemoryCost = config.workerMemory * 20 * config.workerInstances
const total = cpuCost + memoryCost + egressCost + workerCpuCost + workerMemoryCost
return {
webCpuCost: cpuCost,
webMemoryCost: memoryCost,
webEgressCost: egressCost,
workerCpuCost: workerCpuCost,
workerMemoryCost: workerMemoryCost,
total: total
}
}
// Calculate Heroku pricing
calculateHerokuPricing(config) {
const webCost = config.webInstances * 250
const workerCost = config.workerInstances * 50
const total = webCost + workerCost
return {
webCost: webCost,
workerCost: workerCost,
total: total
}
}
// Calculate Fly pricing
calculateFlyPricing(config) {
const cpuCost = config.webInstances * (config.isWebCpuShared ? 14.24 : 28.48)
const egressCost = config.webEgress * 0.02
const workerCost = config.workerInstances * (config.isWorkerCpuShared ? 7.12 : 14.24)
const total = cpuCost + egressCost + workerCost
return {
webCpuCost: cpuCost,
webEgressCost: egressCost,
workerCost: workerCost,
total: total
}
}
// Calculate Render pricing
calculateRenderPricing(config) {
const webCost = config.webInstances * 43.80
const egressCost = config.webEgress * 4.375
const workerCost = config.workerInstances * 25
const total = webCost + egressCost + workerCost
return {
webCost: webCost,
webEgressCost: egressCost,
workerCost: workerCost,
total: total
}
}
// Update UI with calculated prices
updateUI(results) {
// Update Railway UI
this.updateProviderUI('railway', results.railway)
// Update Heroku UI
this.updateProviderUI('heroku', results.heroku)
// Update Fly UI
this.updateProviderUI('fly', results.fly)
// Update Render UI
this.updateProviderUI('render', results.render)
}
// Update UI for a specific provider
updateProviderUI(provider, pricing) {
const providerElement = this.findProviderElement(provider)
if (!providerElement) return
// Update total cost
const totalElement = providerElement.querySelector('.total-cost')
if (totalElement) {
totalElement.textContent = `$${pricing.total.toFixed(2)}`
}
// Update individual cost components based on provider
if (provider === 'railway') {
this.updateElementText(providerElement, '.webCpuCost', pricing.webCpuCost)
this.updateElementText(providerElement, '.webMemoryCost', pricing.webMemoryCost)
this.updateElementText(providerElement, '.webEgressCost', pricing.webEgressCost)
this.updateElementText(providerElement, '.workerCpuCost', pricing.workerCpuCost)
this.updateElementText(providerElement, '.workerMemoryCost', pricing.workerMemoryCost)
} else if (provider === 'heroku') {
this.updateElementText(providerElement, '.webCost', pricing.webCost)
this.updateElementText(providerElement, '.workerCost', pricing.workerCost)
} else if (provider === 'fly') {
this.updateElementText(providerElement, '.webCpuCost', pricing.webCpuCost)
this.updateElementText(providerElement, '.webEgressCost', pricing.webEgressCost)
this.updateElementText(providerElement, '.workerCost', pricing.workerCost)
} else if (provider === 'render') {
this.updateElementText(providerElement, '.webCost', pricing.webCost)
this.updateElementText(providerElement, '.webEgressCost', pricing.webEgressCost)
this.updateElementText(providerElement, '.workerCost', pricing.workerCost)
}
}
// Helper to update element text with formatted price
updateElementText(parent, selector, value) {
const element = parent.querySelector(selector)
if (element) {
element.textContent = `$${value.toFixed(2)}`
}
}
// Find provider element by name
findProviderElement(provider) {
return this.providerTargets.find(el => el.dataset.provider === provider)
}
// Highlight the cheapest provider
highlightCheapestProvider(results) {
const providers = Object.keys(results)
const totals = providers.map(provider => results[provider].total)
// Find the cheapest provider
const minTotal = Math.min(...totals)
const cheapestProviderIndex = totals.indexOf(minTotal)
const cheapestProvider = providers[cheapestProviderIndex]
// Remove highlight from all providers
this.providerTargets.forEach(provider => {
const totalElement = provider.querySelector('.total-cost')
if (totalElement) {
totalElement.classList.remove('text-green-300', 'font-bold')
totalElement.classList.add('text-emerald-400')
}
})
// Add highlight to cheapest provider
const cheapestElement = this.findProviderElement(cheapestProvider)
if (cheapestElement) {
const totalElement = cheapestElement.querySelector('.total-cost')
if (totalElement) {
totalElement.classList.remove('text-emerald-400')
totalElement.classList.add('text-green-300', 'font-bold')
}
}
}
}

View File

@@ -0,0 +1,565 @@
<!-- Inspired by https://judoscale.com/tools/paas-pricing-calculator -->
<div class="bg-slate-900 min-h-screen text-white">
<!-- Header -->
<header class="container mx-auto py-8 px-4">
<h1 class="text-4xl font-bold text-center mb-4">The Ultimate PaaS Pricing Comparison Calculator</h1>
<p class="text-center text-lg mb-12">Compare cloud providers. Find out how much it will cost to run your web app.</p>
</header>
<div class="container mx-auto px-4 pb-16">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Left Column - Configuration -->
<div>
<!-- Team Section -->
<div class="bg-slate-800 rounded-lg p-6 mb-6">
<h2 class="text-xl font-semibold mb-4">Team</h2>
<div class="mb-4">
<div class="flex justify-between mb-2">
<label for="team-size" class="text-sm">Team size: <span id="team-size-value">11</span> members</label>
</div>
<input type="range" id="team-size" min="1" max="50" value="11" class="w-full h-2 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-emerald-500">
<div class="flex justify-between text-xs text-slate-400 mt-1">
<span>1</span>
<span>50</span>
</div>
</div>
</div>
<!-- Web Service Section -->
<div class="bg-slate-800 rounded-lg p-6 mb-6">
<h2 class="text-xl font-semibold mb-4">Web Service</h2>
<!-- CPU -->
<div class="mb-6">
<div class="flex justify-between mb-2">
<label for="web-cpu" class="text-sm">CPU: <span id="web-cpu-value">2</span> cores</label>
<div class="flex space-x-2">
<button id="web-cpu-shared" class="bg-emerald-500 text-white text-xs px-3 py-1 rounded-md">Shared</button>
<button id="web-cpu-dedicated" class="bg-slate-700 text-white text-xs px-3 py-1 rounded-md">Dedicated</button>
</div>
</div>
<input type="range" id="web-cpu" min="0.5" max="16" step="0.5" value="2" class="w-full h-2 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-emerald-500">
<div class="flex justify-between text-xs text-slate-400 mt-1">
<span>0.5</span>
<span>16</span>
</div>
</div>
<!-- Memory -->
<div class="mb-6">
<div class="flex justify-between mb-2">
<label for="web-memory" class="text-sm">Memory: <span id="web-memory-value">2</span> GB</label>
</div>
<input type="range" id="web-memory" min="0.5" max="32" step="0.5" value="2" class="w-full h-2 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-emerald-500">
<div class="flex justify-between text-xs text-slate-400 mt-1">
<span>0.5</span>
<span>32</span>
</div>
</div>
<!-- Instances/Replicas -->
<div class="mb-6">
<div class="flex justify-between mb-2">
<label for="web-instances" class="text-sm">Instances/Replicas: <span id="web-instances-value">5</span></label>
</div>
<input type="range" id="web-instances" min="1" max="30" value="5" class="w-full h-2 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-emerald-500">
<div class="flex justify-between text-xs text-slate-400 mt-1">
<span>1</span>
<span>30</span>
</div>
</div>
<!-- Egress -->
<div class="mb-2">
<div class="flex justify-between mb-2">
<label for="web-egress" class="text-sm">Egress (outbound data): <span id="web-egress-value">200</span> GB</label>
</div>
<input type="range" id="web-egress" min="10" max="2000" value="200" class="w-full h-2 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-emerald-500">
<div class="flex justify-between text-xs text-slate-400 mt-1">
<span>10</span>
<span>2000</span>
</div>
</div>
</div>
<!-- Worker Service Section -->
<div class="bg-slate-800 rounded-lg p-6">
<h2 class="text-xl font-semibold mb-4">Worker Service</h2>
<!-- CPU -->
<div class="mb-6">
<div class="flex justify-between mb-2">
<label for="worker-cpu" class="text-sm">CPU: <span id="worker-cpu-value">1</span> cores</label>
<div class="flex space-x-2">
<button id="worker-cpu-shared" class="bg-emerald-500 text-white text-xs px-3 py-1 rounded-md">Shared</button>
<button id="worker-cpu-dedicated" class="bg-slate-700 text-white text-xs px-3 py-1 rounded-md">Dedicated</button>
</div>
</div>
<input type="range" id="worker-cpu" min="0.5" max="16" step="0.5" value="1" class="w-full h-2 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-emerald-500">
<div class="flex justify-between text-xs text-slate-400 mt-1">
<span>0.5</span>
<span>16</span>
</div>
</div>
<!-- Memory -->
<div class="mb-6">
<div class="flex justify-between mb-2">
<label for="worker-memory" class="text-sm">Memory: <span id="worker-memory-value">1</span> GB</label>
</div>
<input type="range" id="worker-memory" min="0.5" max="32" step="0.5" value="1" class="w-full h-2 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-emerald-500">
<div class="flex justify-between text-xs text-slate-400 mt-1">
<span>0.5</span>
<span>32</span>
</div>
</div>
<!-- Instances/Replicas -->
<div class="mb-2">
<div class="flex justify-between mb-2">
<label for="worker-instances" class="text-sm">Instances/Replicas: <span id="worker-instances-value">3</span></label>
</div>
<input type="range" id="worker-instances" min="1" max="30" value="3" class="w-full h-2 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-emerald-500">
<div class="flex justify-between text-xs text-slate-400 mt-1">
<span>1</span>
<span>30</span>
</div>
</div>
</div>
</div>
<!-- Right Column - Results -->
<div class="bg-slate-800 rounded-lg p-6">
<h2 class="text-lg font-semibold mb-4 uppercase text-slate-400">Plan Comparison</h2>
<div class="grid grid-cols-2 gap-4 mb-4">
<div class="font-semibold">PaaS Provider</div>
<div class="font-semibold text-right">Monthly Cost (USD)</div>
</div>
<!-- Railway -->
<div class="border-t border-slate-700 py-4" data-provider="railway">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center">
<span class="font-medium">Railway</span>
<span class="ml-2 inline-flex items-center justify-center w-5 h-5 bg-slate-700 rounded-full text-xs cursor-help tooltip" data-tooltip="Railway is a modern PaaS that offers simple deployment and scaling.">?</span>
</div>
<div class="text-emerald-400 font-semibold total-cost">$620.00</div>
</div>
<div class="text-sm text-slate-400 mb-1">Team members</div>
<div class="grid grid-cols-2 gap-2 text-sm mb-1">
<div>Web Service - CPU</div>
<div class="text-right web-cpu-cost">$220.00</div>
</div>
<div class="grid grid-cols-2 gap-2 text-sm mb-1">
<div>Web Service - Memory</div>
<div class="text-right web-memory-cost">$200.00</div>
</div>
<div class="grid grid-cols-2 gap-2 text-sm mb-1">
<div>Web Service - Egress</div>
<div class="text-right web-egress-cost">$100.00</div>
</div>
<div class="grid grid-cols-2 gap-2 text-sm mb-1">
<div>Worker Service - CPU</div>
<div class="text-right worker-cpu-cost">$60.00</div>
</div>
<div class="grid grid-cols-2 gap-2 text-sm mb-1">
<div>Worker Service - Memory</div>
<div class="text-right worker-memory-cost">$40.00</div>
</div>
</div>
<!-- Heroku -->
<div class="border-t border-slate-700 py-4" data-provider="heroku">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center">
<span class="font-medium">Heroku</span>
<span class="ml-2 inline-flex items-center justify-center w-5 h-5 bg-slate-700 rounded-full text-xs cursor-help tooltip" data-tooltip="Heroku is a cloud platform that lets companies build, deliver, monitor and scale apps.">?</span>
</div>
<div class="text-emerald-400 font-semibold total-cost">$1,400.00</div>
</div>
<div class="text-sm text-slate-400 mb-1">Team members</div>
<div class="grid grid-cols-2 gap-2 text-sm mb-1">
<div>Web Service - Perf-M (5)</div>
<div class="text-right web-cost">$1,250.00</div>
</div>
<div class="grid grid-cols-2 gap-2 text-sm mb-1">
<div>Web Service - Egress</div>
<div class="text-right">—</div>
</div>
<div class="grid grid-cols-2 gap-2 text-sm mb-1">
<div>Worker Service - Std-2x (3)</div>
<div class="text-right worker-cost">$150.00</div>
</div>
</div>
<!-- Fly -->
<div class="border-t border-slate-700 py-4" data-provider="fly">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center">
<span class="font-medium">Fly</span>
<span class="ml-2 inline-flex items-center justify-center w-5 h-5 bg-slate-700 rounded-full text-xs cursor-help tooltip" data-tooltip="Fly.io is a platform for running full-stack apps and databases close to your users.">?</span>
</div>
<div class="text-emerald-400 font-semibold total-cost">$96.56</div>
</div>
<div class="text-sm text-slate-400 mb-1">Team members</div>
<div class="grid grid-cols-2 gap-2 text-sm mb-1">
<div>Web Service - shared-cpu-2x (5)</div>
<div class="text-right web-cpu-cost">$71.20</div>
</div>
<div class="grid grid-cols-2 gap-2 text-sm mb-1">
<div>Web Service - Egress</div>
<div class="text-right web-egress-cost">$4.00</div>
</div>
<div class="grid grid-cols-2 gap-2 text-sm mb-1">
<div>Worker Service - shared-cpu-1x (3)</div>
<div class="text-right worker-cost">$21.36</div>
</div>
</div>
<!-- Render -->
<div class="border-t border-slate-700 py-4" data-provider="render">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center">
<span class="font-medium">Render</span>
<span class="ml-2 inline-flex items-center justify-center w-5 h-5 bg-slate-700 rounded-full text-xs cursor-help tooltip" data-tooltip="Render is a unified cloud to build and run all your apps and websites with free SSL, a global CDN, private networks and auto deploys from Git.">?</span>
</div>
<div class="text-emerald-400 font-semibold total-cost">$1,169.00</div>
</div>
<div class="text-sm text-slate-400 mb-1">Team members</div>
<div class="grid grid-cols-2 gap-2 text-sm mb-1">
<div>Web Service - Pro Plus (5)</div>
<div class="text-right web-cost">$219.00</div>
</div>
<div class="grid grid-cols-2 gap-2 text-sm mb-1">
<div>Web Service - Egress</div>
<div class="text-right web-egress-cost">$875.00</div>
</div>
<div class="grid grid-cols-2 gap-2 text-sm mb-1">
<div>Worker Service - Standard (3)</div>
<div class="text-right worker-cost">$75.00</div>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
/* Custom slider styling */
input[type="range"] {
-webkit-appearance: none;
height: 8px;
border-radius: 4px;
background: #1e293b;
outline: none;
position: relative;
}
/* This creates the colored track before the thumb */
input[type="range"]::before {
content: '';
position: absolute;
height: 8px;
left: 0;
right: calc(100% - var(--range-progress, 50%));
border-radius: 4px;
background: #10b981;
pointer-events: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: #10b981;
cursor: pointer;
transition: all 0.15s ease;
position: relative;
z-index: 2;
}
input[type="range"]::-webkit-slider-thumb:hover {
transform: scale(1.1);
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.3);
}
input[type="range"]::-moz-range-thumb {
width: 18px;
height: 18px;
border-radius: 50%;
background: #10b981;
cursor: pointer;
border: none;
transition: all 0.15s ease;
position: relative;
z-index: 2;
}
input[type="range"]::-moz-range-progress {
background: #10b981;
border-radius: 4px;
height: 8px;
}
input[type="range"]::-moz-range-thumb:hover {
transform: scale(1.1);
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.3);
}
/* Tooltip styling */
.tooltip {
position: relative;
}
.tooltip:hover::after {
content: attr(data-tooltip);
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 100%;
margin-bottom: 5px;
background: #0f172a;
color: white;
padding: 8px 12px;
border-radius: 6px;
font-size: 12px;
white-space: nowrap;
z-index: 10;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.tooltip:hover::before {
content: "";
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 100%;
border-width: 5px;
border-style: solid;
border-color: #0f172a transparent transparent transparent;
z-index: 10;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Function to update slider progress
function updateSliderProgress(slider) {
const min = parseFloat(slider.min) || 0;
const max = parseFloat(slider.max) || 100;
const value = parseFloat(slider.value) || 0;
const percentage = ((value - min) / (max - min)) * 100;
slider.style.setProperty('--range-progress', `${percentage}%`);
}
// Get all range sliders
const sliders = document.querySelectorAll('input[type="range"]');
// Initialize and add event listeners to all sliders
sliders.forEach(slider => {
updateSliderProgress(slider);
slider.addEventListener('input', () => {
updateSliderProgress(slider);
});
});
// Team size slider
const teamSizeSlider = document.getElementById('team-size');
const teamSizeValue = document.getElementById('team-size-value');
teamSizeSlider.addEventListener('input', function() {
teamSizeValue.textContent = this.value;
updatePricing();
});
// Web Service sliders
const webCpuSlider = document.getElementById('web-cpu');
const webCpuValue = document.getElementById('web-cpu-value');
const webCpuShared = document.getElementById('web-cpu-shared');
const webCpuDedicated = document.getElementById('web-cpu-dedicated');
webCpuSlider.addEventListener('input', function() {
webCpuValue.textContent = this.value;
updatePricing();
});
webCpuShared.addEventListener('click', function() {
webCpuShared.classList.add('bg-emerald-500');
webCpuShared.classList.remove('bg-slate-700');
webCpuDedicated.classList.add('bg-slate-700');
webCpuDedicated.classList.remove('bg-emerald-500');
updatePricing();
});
webCpuDedicated.addEventListener('click', function() {
webCpuDedicated.classList.add('bg-emerald-500');
webCpuDedicated.classList.remove('bg-slate-700');
webCpuShared.classList.add('bg-slate-700');
webCpuShared.classList.remove('bg-emerald-500');
updatePricing();
});
const webMemorySlider = document.getElementById('web-memory');
const webMemoryValue = document.getElementById('web-memory-value');
webMemorySlider.addEventListener('input', function() {
webMemoryValue.textContent = this.value;
updatePricing();
});
const webInstancesSlider = document.getElementById('web-instances');
const webInstancesValue = document.getElementById('web-instances-value');
webInstancesSlider.addEventListener('input', function() {
webInstancesValue.textContent = this.value;
updatePricing();
});
const webEgressSlider = document.getElementById('web-egress');
const webEgressValue = document.getElementById('web-egress-value');
webEgressSlider.addEventListener('input', function() {
webEgressValue.textContent = this.value;
updatePricing();
});
// Worker Service sliders
const workerCpuSlider = document.getElementById('worker-cpu');
const workerCpuValue = document.getElementById('worker-cpu-value');
const workerCpuShared = document.getElementById('worker-cpu-shared');
const workerCpuDedicated = document.getElementById('worker-cpu-dedicated');
workerCpuSlider.addEventListener('input', function() {
workerCpuValue.textContent = this.value;
updatePricing();
});
workerCpuShared.addEventListener('click', function() {
workerCpuShared.classList.add('bg-emerald-500');
workerCpuShared.classList.remove('bg-slate-700');
workerCpuDedicated.classList.add('bg-slate-700');
workerCpuDedicated.classList.remove('bg-emerald-500');
updatePricing();
});
workerCpuDedicated.addEventListener('click', function() {
workerCpuDedicated.classList.add('bg-emerald-500');
workerCpuDedicated.classList.remove('bg-slate-700');
workerCpuShared.classList.add('bg-slate-700');
workerCpuShared.classList.remove('bg-emerald-500');
updatePricing();
});
const workerMemorySlider = document.getElementById('worker-memory');
const workerMemoryValue = document.getElementById('worker-memory-value');
workerMemorySlider.addEventListener('input', function() {
workerMemoryValue.textContent = this.value;
updatePricing();
});
const workerInstancesSlider = document.getElementById('worker-instances');
const workerInstancesValue = document.getElementById('worker-instances-value');
workerInstancesSlider.addEventListener('input', function() {
workerInstancesValue.textContent = this.value;
updatePricing();
});
// Pricing calculation function
function updatePricing() {
// This is a simplified pricing model
// In a real implementation, you would have more complex pricing logic
const teamSize = parseInt(teamSizeSlider.value);
const webCpu = parseFloat(webCpuSlider.value);
const webMemory = parseFloat(webMemorySlider.value);
const webInstances = parseInt(webInstancesSlider.value);
const webEgress = parseInt(webEgressSlider.value);
const workerCpu = parseFloat(workerCpuSlider.value);
const workerMemory = parseFloat(workerMemorySlider.value);
const workerInstances = parseInt(workerInstancesSlider.value);
const isWebCpuShared = webCpuShared.classList.contains('bg-emerald-500');
const isWorkerCpuShared = workerCpuShared.classList.contains('bg-emerald-500');
// Calculate Railway pricing
const railwayCpuCost = webCpu * (isWebCpuShared ? 20 : 30) * webInstances;
const railwayMemoryCost = webMemory * 20 * webInstances;
const railwayEgressCost = webEgress * 0.5;
const railwayWorkerCpuCost = workerCpu * (isWorkerCpuShared ? 20 : 30) * workerInstances;
const railwayWorkerMemoryCost = workerMemory * 20 * workerInstances;
const railwayTotal = railwayCpuCost + railwayMemoryCost + railwayEgressCost + railwayWorkerCpuCost + railwayWorkerMemoryCost;
// Calculate Heroku pricing
const herokuWebCost = webInstances * 250;
const herokuWorkerCost = workerInstances * 50;
const herokuTotal = herokuWebCost + herokuWorkerCost;
// Calculate Fly pricing
const flyCpuCost = webInstances * (isWebCpuShared ? 14.24 : 28.48);
const flyEgressCost = webEgress * 0.02;
const flyWorkerCost = workerInstances * (isWorkerCpuShared ? 7.12 : 14.24);
const flyTotal = flyCpuCost + flyEgressCost + flyWorkerCost;
// Calculate Render pricing
const renderWebCost = webInstances * 43.80;
const renderEgressCost = webEgress * 4.375;
const renderWorkerCost = workerInstances * 25;
const renderTotal = renderWebCost + renderEgressCost + renderWorkerCost;
// Update Railway UI
document.querySelector('[data-provider="railway"] .web-cpu-cost').textContent = `$${railwayCpuCost.toFixed(2)}`;
document.querySelector('[data-provider="railway"] .web-memory-cost').textContent = `$${railwayMemoryCost.toFixed(2)}`;
document.querySelector('[data-provider="railway"] .web-egress-cost').textContent = `$${railwayEgressCost.toFixed(2)}`;
document.querySelector('[data-provider="railway"] .worker-cpu-cost').textContent = `$${railwayWorkerCpuCost.toFixed(2)}`;
document.querySelector('[data-provider="railway"] .worker-memory-cost').textContent = `$${railwayWorkerMemoryCost.toFixed(2)}`;
document.querySelector('[data-provider="railway"] .total-cost').textContent = `$${railwayTotal.toFixed(2)}`;
// Update Heroku UI
document.querySelector('[data-provider="heroku"] .web-cost').textContent = `$${herokuWebCost.toFixed(2)}`;
document.querySelector('[data-provider="heroku"] .worker-cost').textContent = `$${herokuWorkerCost.toFixed(2)}`;
document.querySelector('[data-provider="heroku"] .total-cost').textContent = `$${herokuTotal.toFixed(2)}`;
// Update Fly UI
document.querySelector('[data-provider="fly"] .web-cpu-cost').textContent = `$${flyCpuCost.toFixed(2)}`;
document.querySelector('[data-provider="fly"] .web-egress-cost').textContent = `$${flyEgressCost.toFixed(2)}`;
document.querySelector('[data-provider="fly"] .worker-cost').textContent = `$${flyWorkerCost.toFixed(2)}`;
document.querySelector('[data-provider="fly"] .total-cost').textContent = `$${flyTotal.toFixed(2)}`;
// Update Render UI
document.querySelector('[data-provider="render"] .web-cost').textContent = `$${renderWebCost.toFixed(2)}`;
document.querySelector('[data-provider="render"] .web-egress-cost').textContent = `$${renderEgressCost.toFixed(2)}`;
document.querySelector('[data-provider="render"] .worker-cost').textContent = `$${renderWorkerCost.toFixed(2)}`;
document.querySelector('[data-provider="render"] .total-cost').textContent = `$${renderTotal.toFixed(2)}`;
// Highlight the cheapest option
const providers = ['railway', 'heroku', 'fly', 'render'];
const totals = [railwayTotal, herokuTotal, flyTotal, renderTotal];
// Find the cheapest provider
const minTotal = Math.min(...totals);
const cheapestProviderIndex = totals.indexOf(minTotal);
const cheapestProvider = providers[cheapestProviderIndex];
// Remove highlight from all providers
providers.forEach(provider => {
document.querySelector(`[data-provider="${provider}"] .total-cost`).classList.remove('text-green-300', 'font-bold');
document.querySelector(`[data-provider="${provider}"] .total-cost`).classList.add('text-emerald-400');
});
// Add highlight to cheapest provider
document.querySelector(`[data-provider="${cheapestProvider}"] .total-cost`).classList.remove('text-emerald-400');
document.querySelector(`[data-provider="${cheapestProvider}"] .total-cost`).classList.add('text-green-300', 'font-bold');
}
// Initialize pricing on page load
updatePricing();
});
</script>

View File

@@ -1,6 +0,0 @@
<div class="container mx-auto mt-24 mb-24">
<div class="prose-lg max-w-lg container mx-auto rounded-lg py-24 p-12 bg-base-100">
<%= @markdown.render(@content).html_safe %>
</div>
</div>
<%= render "static/landing_page/footer" %>

View File

@@ -106,6 +106,7 @@ Rails.application.routes.draw do
get "manifest" => "rails/pwa#manifest", as: :pwa_manifest
get "async_render" => "async_renderer#async_render"
get "/pricing_calculator", to: "static#calculator"
# Public marketing homepage
if Rails.application.config.local_mode
get "/github_token", to: "local/pages#github_token"