Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ROI Calculator #814

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions website/_data/value-calculator.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
- id: team-size
title: Team size
info: 6,000 min included = 6,000 min on xsmall 6,000 min included = 6,000 min on xsmall
min: 1
max: 50
maxlength: 4
value: 30
unit: developers
containerClass: bg-[#E8F7FE] mt-6 lg:mt-8 pl-3 pr-3 py-4 rounded-md
sliderStyle: "height: 8px"
titleClass: font-semibold lg:leading-6 text-base lg:text-lg

- id: avg-build-time
title: Avg Build Time
info: 6,000 min included = 6,000 min on xsmall 6,000 min included = 6,000 min on xsmall
min: 1
max: 60
maxlength: 4
value: 25
unit: minute
containerClass: mt-8

- id: build-frequency
title: Frequency of builds
info: 6,000 min included = 6,000 min on xsmall 6,000 min included = 6,000 min on xsmall
min: 1
max: 30
maxlength: 4
value: 5
unit: / dev / day

- id: developer-salary
title: Developer salary per year
info: 6,000 min included = 6,000 min on xsmall 6,000 min included = 6,000 min on xsmall
link: https://money.usnews.com/careers/best-jobs/software-developer/salary
linkLabel: (Source)
min: 50000
max: 350000
step: 10000
maxlength: 6
value: 120000
prefixUnit: $
unit: / developer
advanced: true
containerClass: mt-8

- id: discount-factor
title: Time savings discount factor
info: 6,000 min included = 6,000 min on xsmall 6,000 min included = 6,000 min on xsmall
min: 1
max: 100
value: 50
unit: "%"
advanced: true

- id: ci-price
title: CI compute unit price
info: 6,000 min included = 6,000 min on xsmall 6,000 min included = 6,000 min on xsmall
min: 0
max: 0.07
step: 0.001
maxlength: 8
value: 0.008
prefixUnit: $
unit: / minute
advanced: true

- id: speedup-factor
title: Earthly build speedup factor
info: 6,000 min included = 6,000 min on xsmall 6,000 min included = 6,000 min on xsmall
min: 1
max: 5
step: 0.5
maxlength: 4
value: 2.5
unit: X
advanced: true

- id: parallel-tasks
title: No. of parallel tasks
info: 6,000 min included = 6,000 min on xsmall 6,000 min included = 6,000 min on xsmall
min: 1
max: 10
maxlength: 4
value: 5
unit: / build
advanced: true
1 change: 1 addition & 0 deletions website/_includes/pricing/v2/cloud.html
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,6 @@
</div>
{% include /pricing/v2/tier-5.html tab="cloud" %}

{% include /value-calculator/index.html %}
{% include /pricing/v2/compute-cost-table.html %}
</div>
22 changes: 20 additions & 2 deletions website/_includes/pricing/v2/compute-cost-table.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
<div id="compute-pricing-container">
<div id="compute-pricing" class="text-3xl font-semibold mt-10 mb-4">Compute Pricing</div>
<div id="compute-pricing" class="cursor-pointer flex items-center justify-between text-3xl font-semibold mt-10" onclick="toggleComputePricing(this)">
<span>Compute Pricing</span>
<img class="advanced-chevron mr-2" src="assets/svg/chevron.svg" alt="Chevron icon" />
</div>

<div>
<div class="leading-9 mb-5 text-gray-600 text-lg">We include a generous number of minutes with every
plan that we offer.<br>However, if you exceed the included minutes, overages will be charged
according to <b>our low-cost, zero-margin</b> pricing below.</div>
Expand Down Expand Up @@ -168,4 +173,17 @@
</div>
</div>
</div>
</div>
</div>
</div>

<script>
function toggleComputePricing(element) {
if (element.nextElementSibling.classList.contains("open")) {
element.classList.remove("open")
element.nextElementSibling.classList.remove("open")
} else {
element.classList.add("open")
element.nextElementSibling.classList.add("open")
}
}
</script>
186 changes: 186 additions & 0 deletions website/_includes/value-calculator/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
<link rel="stylesheet" href="/assets/css/subpage.css" />

<div class="value-calculator">
<div class="font-semibold mt-10 text-3xl">Value Calculator</div>

<div class="leading-9 mt-4 text-gray-600 text-lg">
Lorem ipsum dolor sit amet consectetur. Etiam nulla nulla ut mollis elit tortor consectetur posuere aliquam.
Cursus<br />netus fermentum sed praesent nec ultricies magna bibendum quam.
</div>

<div class="bg-[#f9fafc] flex flex-col lg:flex-row gap-10 mt-6 px-4 lg:px-0 pt-8 pb-8 lg:pb-10 rounded-md">
<div class="lg:w-[47%] h-fit bg-white lg:ml-9 pb-10 pt-8 px-5 rounded-md">
<div class="border-b font-semibold pb-6 text-[#475569] text-xl lg:text-2xl text-center tracking-tight">Annual Savings</div>

<div class="border-b flex py-6">
<div class="flex-1">
<div class="flex font-semibold gap-0.5 justify-center text-2xl">
<span class="leading-none text-gray-600 text-sm lg:text-lg">$</span>
<span id="ci-savings" class="break-all leading-relaxed text-lg lg:text-[32px] tracking-tighter">23,220</span>
</div>

<div class="font-semibold mt-2 text-[#6b7280] text-sm lg:text-base text-center">CI infrastructure savings</div>
</div>

<div class="border-r" style="width: 1px; height: 72px"></div>

<div class="flex-1">
<div class="flex font-semibold gap-0.5 justify-center text-2xl">
<span class="leading-none text-gray-600 text-sm lg:text-lg">$</span>
<span id="developer-value" class="break-all leading-relaxed text-lg lg:text-[32px] tracking-tighter">302,344</span>
</div>

<div class="font-semibold mt-2 text-[#6b7280] text-sm lg:text-base text-center">Developer value saved</div>
</div>
</div>

<div class="py-6">
<div class="flex font-semibold gap-0.5 justify-center text-2xl">
<span class="leading-none text-gray-600 text-sm lg:text-lg">$</span>
<span id="earthly-price" class="break-all leading-relaxed text-lg lg:text-[32px] tracking-tighter">19,714</span>
</div>

<div class="font-semibold mt-2 text-[#6b7280] text-sm lg:text-base text-center">Earthly Cloud total price</div>
</div>

<div class="bg-[#E8F7FE] py-4 lg:py-8 rounded-md">
<div class="font-semibold text-center text-lg lg:text-xl tracking-tight">Return on investment</div>

<div class="flex mt-4 lg:mt-8">
<div class="flex-1">
<div class="flex font-semibold gap-0.5 justify-center text-2xl">
<span class="leading-none text-green-700 text-sm lg:text-lg">$</span>
<span id="value-saved" class="break-all leading-snug text-xl lg:text-[40px] text-green-700 tracking-tighter">305,850</span>
</div>

<div class="font-semibold mt-2 text-[#6b7280] text-sm lg:text-base text-center">Value saved</div>
</div>

<div class="border-r" style="width: 1px; height: 72px"></div>

<div class="flex-1">
<div id="developers" class="break-all font-semibold leading-snug text-xl lg:text-[40px] text-center text-green-700 tracking-tighter">
2.55
</div>
<div class="font-semibold mt-2 text-[#6b7280] text-sm lg:text-base text-center">Developers</div>
</div>
</div>
</div>
</div>

<div class="flex-1 lg:mr-9">
<div class="bg-white pb-10 pt-6 px-3.5 rounded-md">
<div class="font-semibold text-[#475569] text-lg lg:text-xl tracking-tight">Your builds</div>

{% include /value-calculator/inputs.html %}
</div>

<div class="bg-white mt-5 px-3.5 rounded-md advanced-inputs">
<div class="cursor-pointer flex font-semibold items-center justify-between py-4 text-[#475569] text-lg lg:text-xl tracking-tight" onclick="toggleAdvancedInputs()">
<span>Advanced</span>
<img class="advanced-chevron" src="assets/svg/chevron.svg" alt="Chevron icon" />
</div>

<div>
{% include /value-calculator/inputs.html advanced=true %}
</div>
</div>
</div>
</div>
</div>

<script>
// All input values are stored here
var inputValues = {}

function vcCalculateValues() {
// Sanitize values (Empty string & decimal point to 0)
Object.keys(inputValues).forEach(function (key, index) {
let newValue = inputValues[key]
inputValues[key] = ["", "."].includes(newValue) ? 0 : newValue.endsWith(".") ? (newValue + "0") : newValue
})

// Calculate reusable variables
const buildMinutesWithoutEarthly =
inputValues["avg-build-time"] *
inputValues["build-frequency"] *
inputValues["parallel-tasks"] *
21.5 *
inputValues["team-size"]

const ciInfrastructureSavingsMonthly =
buildMinutesWithoutEarthly * inputValues["ci-price"] -
(buildMinutesWithoutEarthly / inputValues["speedup-factor"]) * inputValues["ci-price"]

const developerTimeSavedMonthly =
inputValues["avg-build-time"] *
inputValues["build-frequency"] *
inputValues["team-size"] *
(1 - inputValues["discount-factor"] / 100) *
(1 - 1 / inputValues["speedup-factor"]) *
21.5

const developerValueSavedMonthly = ((inputValues["developer-salary"] / 240 / 8) * developerTimeSavedMonthly) / 60

// Constant values
const EARTHLY_CLOUD_DEVELOPER_UNIT = 49.17
const EARTHLY_XSMALL_COMPUTE_UNIT = 0.0011

const earthlyCloudTotalPriceMonthly =
inputValues["team-size"] * EARTHLY_CLOUD_DEVELOPER_UNIT +
EARTHLY_XSMALL_COMPUTE_UNIT * (Math.max(0, buildMinutesWithoutEarthly / inputValues["speedup-factor"] - (50000 + 4000 * inputValues["team-size"]) / 2) * 2)

const valueSaved = (developerValueSavedMonthly + ciInfrastructureSavingsMonthly - earthlyCloudTotalPriceMonthly) * 12

// Write the values to text fields
document.getElementById("ci-savings").innerText = (ciInfrastructureSavingsMonthly * 12).toLocaleString(undefined, { maximumFractionDigits: 0 })
document.getElementById("developer-value").innerText = (developerValueSavedMonthly * 12).toLocaleString(undefined, { maximumFractionDigits: 0 })
document.getElementById("earthly-price").innerText = (earthlyCloudTotalPriceMonthly * 12).toLocaleString(undefined, { maximumFractionDigits: 0 })
document.getElementById("value-saved").innerText = valueSaved.toLocaleString(undefined, { maximumFractionDigits: 0 })
document.getElementById("developers").innerText = (valueSaved / inputValues["developer-salary"]).toLocaleString(undefined, { maximumFractionDigits: 2 })
}

// Handle manual text input in the fields
function vcHandleInputChange(input) {
const { id, min, max, value } = input

// Escape non-number chars & add restriction to input fields
var newValue = input.value.replace(/[^\d.]/, "")
if (!["", "."].includes(newValue) && (newValue < 0 || isNaN(newValue))) return input.value = inputValues[id.replace("-input", "")]
input.value = newValue

// Update slider accordingly
const slider = document.getElementById(id.replace("-input", ""))
slider.value = ["", "."].includes(newValue) ? 0 : newValue.endsWith(".") ? (newValue + "0") : newValue
slider.style.backgroundSize = ((slider.value - slider.min) * 100) / (slider.max - slider.min) + "% 100%"

inputValues[id.replace("-input", "")] = newValue
vcCalculateValues()
}

function vcHandleSliderChange(slider) {
const { min, max, value } = slider
slider.style.backgroundSize = ((value - min) * 100) / (max - min) + "% 100%"

// Update input value
document.getElementById(slider.id + "-input").value = value

inputValues[slider.id] = slider.value
vcCalculateValues()
}

function toggleAdvancedInputs() {
const advancedInputs = document.getElementsByClassName("advanced-inputs")
if (advancedInputs[0].classList.contains("open")) {
advancedInputs[0].classList.remove("open")
} else {
advancedInputs[0].classList.add("open")
}
}

document.addEventListener("DOMContentLoaded", function () {
document.querySelectorAll(".value-calculator .slider").forEach((slider) => {
vcHandleSliderChange(slider)
})
})
</script>
51 changes: 51 additions & 0 deletions website/_includes/value-calculator/inputs.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{% assign inputs = site.data.value-calculator | where: "advanced", include.advanced %}
{% for item in inputs %}
<div class="mt-6 pl-1 text-sm lg:text-base {{ item.containerClass }}">
<div class="flex gap-2 items-center relative {{ item.titleClass }}">
<span>{{ item.title }}</span>

{% if item.info %}
<div class="cursor-pointer included-minutes-tooltip left-align">
<img src="assets/svg/info.svg" alt="Info icon" />
<span class="w-60 lg:w-80 cursor-default font-normal text-sm tracking-tight z-10">{{ item.info }}</span>
</div>
{% endif %}

{% if item.link %}
<a class="flex font-semibold gap-1 items-center text-[#2d7e5d] text-xs" href="{{ item.link }}" target="_blank">
<span>{{ item.linkLabel }}</span>
<img class="w-4 h-4" src="assets/svg/link.svg" alt="Link icon" />
</a>
{% endif %}
</div>

<div class="flex flex-col lg:flex-row lg:items-center gap-5 lg:gap-4 mt-4 lg:mt-0">
<input
id="{{ item.id }}"
class="slider"
style="{{ item.sliderStyle }}"
type="range"
min="{{ item.min }}"
max="{{ item.max }}"
step="{{ item.step }}"
value="{{ item.value }}"
oninput="vcHandleSliderChange(this)"
/>

<div class="flex items-center gap-1">
{% if item.prefixUnit %}
<span>{{ item.prefixUnit }}</span>
{% endif %}
<input
id="{{ item.id }}-input"
min="{{ item.min }}"
max="{{ item.max }}"
maxlength="{{ item.maxlength }}"
value="{{ item.value }}"
oninput="vcHandleInputChange(this)"
/>
<span>{{ item.unit }}</span>
</div>
</div>
</div>
{% endfor %}
2 changes: 1 addition & 1 deletion website/assets/css/index.46bb9c26.css

Large diffs are not rendered by default.

Loading
Loading