diff --git a/Projects/Startup KPI Dashboard/README.md b/Projects/Startup KPI Dashboard/README.md new file mode 100644 index 0000000..acb3fe1 --- /dev/null +++ b/Projects/Startup KPI Dashboard/README.md @@ -0,0 +1,30 @@ +# Startup KPI Dashboard + +An advanced, interactive SaaS metric center designed for startups to track and simulate key performance indicators (KPIs) like Monthly Recurring Revenue (MRR), Customer Acquisition Cost (CAC), Lifetime Value (LTV), Churn Rate, and CAC Payback periods. + +## Core Features + +- **Granular SaaS Inputs**: Customize current operational metrics: + * **Current MRR ($)**: Starting baseline monthly recurring revenue. + * **New MRR / Month ($)**: Average monthly revenue from new signups. + * **Monthly Churn Rate (%)**: Slider to simulate percentage of customer cancellations. + * **CAC ($)**: Marketing and sales cost to acquire a single customer. + * **ARPA ($/mo)**: Average Revenue Per Account (monthly contract size). +- **Core SaaS Metrics Calculator**: Real-time evaluation of: + * **LTV (Customer Lifetime Value)**: computed as `ARPA / Churn Rate`. + * **LTV : CAC Ratio**: computed as `LTV / CAC`. Indicates customer unit economics viability (Red < 1.5x, Amber 1.5x - 3.0x, Emerald > 3.0x). + * **CAC Payback Period**: computed as `CAC / ARPA` (Months to recover customer acquisition costs). + * **Net MRR Growth**: tracks growth accounting trends. +- **Dual-Axis Projection Chart**: A beautiful responsive SVG chart projecting MRR (growth line) and Customer Count (growth bars) over a 12-month timeline. +- **Monthly Ledgers**: Tabular view of detailed SaaS growth parameters. +- **Scenario Vault**: Save and compare models (e.g. "Conservative Target", "Base Growth", "High-Scale Performance") in `localStorage`. + +## Run it + +Open `index.html` in any modern web browser. + +## Technical Details + +- **HTML5 & CSS3**: Responsive 3-column dashboard, range sliders styling, glassmorphism templates, and CSS gradients. +- **Vanilla JavaScript**: SaaS formulas engine, dynamic SVG pathing (coordinate projection mapping for line curves and bar rect elements), tooltip tracking, and local storage sync. +- **Storage**: JSON models mapped and loaded from `localStorage`. diff --git a/Projects/Startup KPI Dashboard/index.html b/Projects/Startup KPI Dashboard/index.html new file mode 100644 index 0000000..cb517f0 --- /dev/null +++ b/Projects/Startup KPI Dashboard/index.html @@ -0,0 +1,218 @@ + + + + + + Startup KPI Dashboard + + + + + + + + + +
+ + +
+
+ + +
+
+ Annual Run Rate (ARR) + $0 +
+
+
+
+ + +
+ + + + + +
+ + +
+ +
+ LTV : CAC Ratio + 0.0x + Target > 3.0x +
+ +
+ Lifetime Value (LTV) + $0 + Projected gross value +
+ +
+ CAC Payback Period + 0.0 mo + Target < 12 months +
+ +
+ Annual Recurring Revenue + $0 + MRR x 12 ARR +
+ +
+ + +
+
+

12-Month SaaS MRR & Customers Projection

+
+ Projected MRR + Total Customers +
+
+ +
+ + + +
+
+ + +
+
+

SaaS Monthly Growth Accountings

+
+
+ + + + + + + + + + + + + + + +
MonthStarting CustomersStarting MRRNew MRR AddedChurned MRREnding CustomersEnding MRR
+
+
+ +
+ + + + +
+ + + + + + + diff --git a/Projects/Startup KPI Dashboard/project.json b/Projects/Startup KPI Dashboard/project.json new file mode 100644 index 0000000..f6ccc51 --- /dev/null +++ b/Projects/Startup KPI Dashboard/project.json @@ -0,0 +1,17 @@ +{ + "title": "Startup KPI Dashboard", + "description": "An interactive SaaS metrics center tracking MRR, churn rate, customer lifetime value (LTV), acquisition cost (CAC), and payback periods.", + "author": { + "name": "Sujal", + "github": "Sujal" + }, + "tags": [ + "finance", + "saas", + "dashboard", + "vanilla-js", + "localstorage" + ], + "entry": "index.html", + "thumbnail": "thumbnail.svg" +} diff --git a/Projects/Startup KPI Dashboard/script.js b/Projects/Startup KPI Dashboard/script.js new file mode 100644 index 0000000..f6dfd48 --- /dev/null +++ b/Projects/Startup KPI Dashboard/script.js @@ -0,0 +1,695 @@ +// Startup KPI Dashboard - Interaction Logic + +// Predefined Scenario Snapshot Templates +const DEFAULT_SCENARIOS = [ + { + id: "sc-baseline", + name: "SaaS Baseline Plan", + mrr: 50000, + newMrr: 8000, + arpa: 100, + cac: 600, + churn: 5 + }, + { + id: "sc-product-led", + name: "Low-Touch / Product-Led", + mrr: 120000, + newMrr: 15000, + arpa: 30, + cac: 90, + churn: 3 + }, + { + id: "sc-enterprise", + name: "Enterprise High-Touch", + mrr: 25000, + newMrr: 10000, + arpa: 2000, + cac: 12000, + churn: 1 + } +]; + +// Month names list +const MONTHS = ["Baseline", "Month 1", "Month 2", "Month 3", "Month 4", "Month 5", "Month 6", "Month 7", "Month 8", "Month 9", "Month 10", "Month 11", "Month 12"]; + +// App State +let scenarios = []; +let activeScenarioId = "sc-baseline"; +let monthlyProjections = []; + +// DOM Elements +const txtMrr = document.getElementById("txt-mrr"); +const txtNewMrr = document.getElementById("txt-new-mrr"); +const txtArpa = document.getElementById("txt-arpa"); +const txtCac = document.getElementById("txt-cac"); +const sliderChurn = document.getElementById("slider-churn"); +const lblChurnVal = document.getElementById("lbl-churn-val"); + +const lblHeaderArr = document.getElementById("lbl-header-arr"); +const lblPostMoney = document.getElementById("lbl-post-money"); +const lblTotalShares = document.getElementById("lbl-total-shares"); +const lblEsopPct = document.getElementById("lbl-esop-pct"); +const lblDilution = document.getElementById("lbl-dilution"); + +const svgElement = document.getElementById("growth-chart-svg"); +const chartContainer = document.getElementById("chart-container"); +const forecastTableBody = document.getElementById("forecast-table-body"); + +const txtScenarioName = document.getElementById("txt-scenario-name"); +const btnSaveScenario = document.getElementById("btn-save-scenario"); +const scenarioList = document.getElementById("scenario-list"); +const btnResetWorkspace = document.getElementById("btn-reset-workspace"); +const recommendationsFeed = document.getElementById("recommendations-feed"); + +// CREATE TOOLTIP +const tooltip = document.createElement("div"); +tooltip.className = "chart-tooltip"; +document.body.appendChild(tooltip); + +// INITIALIZATION +window.addEventListener("DOMContentLoaded", () => { + loadScenarios(); + setupEventListeners(); + loadScenarioDetails(activeScenarioId); +}); + +// Load saved snapshots +function loadScenarios() { + const stored = localStorage.getItem("kpisync_scenarios"); + if (stored) { + try { + scenarios = JSON.parse(stored); + } catch (e) { + console.error("Error parsing scenarios", e); + scenarios = [...DEFAULT_SCENARIOS]; + } + } else { + scenarios = [...DEFAULT_SCENARIOS]; + saveToStorage(false); + } +} + +function saveToStorage(shouldUpdate = true) { + localStorage.setItem("kpisync_scenarios", JSON.stringify(scenarios)); + if (shouldUpdate) { + renderScenarioList(); + } +} + +// Event Listeners setup +function setupEventListeners() { + const inputs = [txtMrr, txtNewMrr, txtArpa, txtCac]; + + inputs.forEach(input => { + input.addEventListener("input", () => { + activeScenarioId = null; + updateActiveScenarioHighlight(); + calculateAndRender(); + }); + }); + + sliderChurn.addEventListener("input", () => { + lblChurnVal.textContent = `${sliderChurn.value}%`; + activeScenarioId = null; + updateActiveScenarioHighlight(); + calculateAndRender(); + }); + + btnSaveScenario.addEventListener("click", saveCurrentScenario); + btnResetWorkspace.addEventListener("click", resetWorkspace); + window.addEventListener("resize", renderChart); +} + +// MAIN PROJECTIONS ENGINE +function calculateAndRender() { + // 1. Read input values + const mrr = Math.max(0, Number(txtMrr.value) || 0); + const newMrr = Math.max(0, Number(txtNewMrr.value) || 0); + const arpa = Math.max(1, Number(txtArpa.value) || 1); + const cac = Math.max(1, Number(txtCac.value) || 1); + const churn = Math.max(0, Math.min(100, Number(sliderChurn.value) || 0)); + + // 2. Compute Unit Economics Metrics + // Customer Lifetime Value (LTV) = ARPA / Churn + let ltv = 0; + let ltvCac = 0; + + if (churn > 0) { + ltv = arpa / (churn / 100); + ltvCac = ltv / cac; + } else { + ltv = Infinity; + ltvCac = Infinity; + } + + // CAC Payback Period (mo) = CAC / ARPA + const payback = cac / arpa; + + // Render Stats Grid + // LTV:CAC display + const lblLtvCac = document.getElementById("lbl-ltv-cac"); + const lblLtvCacSub = document.getElementById("lbl-ltv-cac-sub"); + if (ltvCac === Infinity) { + lblLtvCac.textContent = "∞ (Infinite)"; + lblLtvCac.className = "m-val text-emerald"; + lblLtvCacSub.textContent = "0% Churn economics"; + } else { + lblLtvCac.textContent = `${ltvCac.toFixed(1)}x`; + if (ltvCac < 1.5) { + lblLtvCac.className = "m-val text-rose"; + lblLtvCacSub.textContent = "Danger unit economics"; + } else if (ltvCac < 3.0) { + lblLtvCac.className = "m-val text-amber"; + lblLtvCacSub.textContent = "Improvement required"; + } else { + lblLtvCac.className = "m-val text-emerald"; + lblLtvCacSub.textContent = "Healthy unit economics"; + } + } + + // LTV display + const lblLtv = document.getElementById("lbl-ltv"); + lblLtv.textContent = ltv === Infinity ? "∞" : formatCurrency(ltv); + + // Payback display + const lblPayback = document.getElementById("lbl-payback"); + const lblPaybackSub = document.getElementById("lbl-payback-sub"); + lblPayback.textContent = `${payback.toFixed(1)} mo`; + if (payback <= 12) { + lblPayback.className = "m-val text-emerald"; + lblPaybackSub.textContent = "Fast payback pace"; + } else if (payback <= 18) { + lblPayback.className = "m-val text-amber"; + lblPaybackSub.textContent = "Moderate payback pace"; + } else { + lblPayback.className = "m-val text-rose"; + lblPaybackSub.textContent = "Slow payback pace"; + } + + // ARR display + const lblArr = document.getElementById("lbl-arr"); + const currentArr = mrr * 12; + lblArr.textContent = formatCurrency(currentArr); + lblHeaderArr.textContent = formatCurrency(currentArr); + + // 3. Generate 12-Month Projections + monthlyProjections = []; + let currentCustomers = mrr / arpa; + let currentMrr = mrr; + + // Month 0 (Starting Baseline) + monthlyProjections.push({ + monthIndex: 0, + monthName: MONTHS[0], + startingCustomers: currentCustomers, + startingMrr: currentMrr, + newCustomers: 0, + newMrr: 0, + churnedCustomers: 0, + churnedMrr: 0, + endingCustomers: currentCustomers, + endingMrr: currentMrr + }); + + for (let m = 1; m <= 12; m++) { + const startCustomers = currentCustomers; + const startMrr = currentMrr; + + // SaaS growth accountings + const newCustomersAdded = newMrr / arpa; + const churnedCust = startCustomers * (churn / 100); + + currentCustomers = startCustomers + newCustomersAdded - churnedCust; + if (currentCustomers < 0) currentCustomers = 0; + + const churnedRevenue = startMrr * (churn / 100); + currentMrr = startMrr + newMrr - churnedRevenue; + if (currentMrr < 0) currentMrr = 0; + + monthlyProjections.push({ + monthIndex: m, + monthName: MONTHS[m], + startingCustomers: startCustomers, + startingMrr: startMrr, + newCustomers: newCustomersAdded, + newMrr: newMrr, + churnedCustomers: churnedCust, + churnedMrr: churnedRevenue, + endingCustomers: currentCustomers, + endingMrr: currentMrr + }); + } + + // 4. Populate Ledgers Table + renderForecastTable(); + + // 5. Draw Dual-Axis Forecasting chart + renderChart(); + + // 6. Draw scenario lists + renderScenarioList(); + + // 7. Dynamic recommendations + renderRecommendations(ltvCac, payback, churn, arpa); +} + +// Monthly LEDGERS Data Table +function renderForecastTable() { + forecastTableBody.innerHTML = ""; + // Skip month 0 in display for clean 12-month projections sheet, or display all + monthlyProjections.forEach((row, i) => { + const tr = document.createElement("tr"); + tr.innerHTML = ` + ${row.monthName} + ${formatNumber(row.startingCustomers)} + ${formatCurrency(row.startingMrr)} + +${formatCurrency(row.newMrr)} + -${formatCurrency(row.churnedMrr)} + ${formatNumber(row.endingCustomers)} + ${formatCurrency(row.endingMrr)} + `; + forecastTableBody.appendChild(tr); + }); +} + +// Draw Dual Axis SVG chart Visuals (MRR Line + Customers Bars) +function renderChart() { + svgElement.innerHTML = ""; + + const w = svgElement.clientWidth || 800; + const h = 320; + const paddingLeft = 70; + const paddingRight = 70; + const paddingTop = 30; + const paddingBottom = 40; + + const chartW = w - paddingLeft - paddingRight; + const chartH = h - paddingTop - paddingBottom; + + // Calculate scales + // Left Axis: MRR ($) + const maxMrr = Math.max(...monthlyProjections.map(d => d.endingMrr), 1000); + const yMaxLeft = maxMrr * 1.1; + + // Right Axis: Customers count + const maxCust = Math.max(...monthlyProjections.map(d => d.endingCustomers), 10); + const yMaxRight = maxCust * 1.1; + + // Draw Grid Lines (Left Y axis) + const gridCountY = 5; + for (let i = 0; i <= gridCountY; i++) { + const yValLeft = yMaxLeft * (i / gridCountY); + const yPos = paddingTop + chartH - (yValLeft / yMaxLeft) * chartH; + + // Grid tick lines + const line = document.createElementNS("http://www.w3.org/2000/svg", "line"); + line.setAttribute("x1", paddingLeft); + line.setAttribute("y1", yPos); + line.setAttribute("x2", w - paddingRight); + line.setAttribute("y2", yPos); + line.setAttribute("class", "grid-line"); + svgElement.appendChild(line); + + // Left Y label (MRR) + const textLeft = document.createElementNS("http://www.w3.org/2000/svg", "text"); + textLeft.setAttribute("x", paddingLeft - 10); + textLeft.setAttribute("y", yPos + 4); + textLeft.setAttribute("text-anchor", "end"); + textLeft.setAttribute("class", "axis-label"); + textLeft.textContent = formatCurrencyShort(yValLeft); + svgElement.appendChild(textLeft); + + // Right Y label (Customers count) + const yValRight = yMaxRight * (i / gridCountY); + const textRight = document.createElementNS("http://www.w3.org/2000/svg", "text"); + textRight.setAttribute("x", w - paddingRight + 10); + textRight.setAttribute("y", yPos + 4); + textRight.setAttribute("text-anchor", "start"); + textRight.setAttribute("class", "axis-label"); + textRight.textContent = formatNumberShort(yValRight); + svgElement.appendChild(textRight); + } + + // Draw X axis ticks + monthlyProjections.forEach((d, i) => { + const xPos = paddingLeft + (i / 12) * chartW; + + const text = document.createElementNS("http://www.w3.org/2000/svg", "text"); + text.setAttribute("x", xPos); + text.setAttribute("y", paddingTop + chartH + 20); + text.setAttribute("text-anchor", "middle"); + text.setAttribute("class", "axis-label"); + text.textContent = d.monthName === "Baseline" ? "Base" : `M${d.monthIndex}`; + svgElement.appendChild(text); + }); + + // Axis lines + const xAxis = document.createElementNS("http://www.w3.org/2000/svg", "line"); + xAxis.setAttribute("x1", paddingLeft); + xAxis.setAttribute("y1", paddingTop + chartH); + xAxis.setAttribute("x2", w - paddingRight); + xAxis.setAttribute("y2", paddingTop + chartH); + xAxis.setAttribute("class", "axis-line"); + svgElement.appendChild(xAxis); + + const yAxisLeft = document.createElementNS("http://www.w3.org/2000/svg", "line"); + yAxisLeft.setAttribute("x1", paddingLeft); + yAxisLeft.setAttribute("y1", paddingTop); + yAxisLeft.setAttribute("x2", paddingLeft); + yAxisLeft.setAttribute("y2", paddingTop + chartH); + yAxisLeft.setAttribute("class", "axis-line"); + svgElement.appendChild(yAxisLeft); + + const yAxisRight = document.createElementNS("http://www.w3.org/2000/svg", "line"); + yAxisRight.setAttribute("x1", w - paddingRight); + yAxisRight.setAttribute("y1", paddingTop); + yAxisRight.setAttribute("x2", w - paddingRight); + yAxisRight.setAttribute("y2", paddingTop + chartH); + yAxisRight.setAttribute("class", "axis-line"); + svgElement.appendChild(yAxisRight); + + // Draw Customers Bars (Right Y-axis scale) + const barW = Math.max(5, (chartW / 13) * 0.5); + monthlyProjections.forEach((d, i) => { + const x = paddingLeft + (i / 12) * chartW; + const barHeight = (d.endingCustomers / yMaxRight) * chartH; + const y = paddingTop + chartH - barHeight; + + const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); + rect.setAttribute("x", x - barW / 2); + rect.setAttribute("y", y); + rect.setAttribute("width", barW); + rect.setAttribute("height", barHeight); + rect.setAttribute("class", "chart-bar"); + svgElement.appendChild(rect); + + // Event tooltips hook + rect.addEventListener("mouseenter", (e) => { + showTooltip(e, ` + ${d.monthName} Details
+ Ending Customers: ${formatNumber(d.endingCustomers)}
+ Ending MRR: ${formatCurrency(d.endingMrr)} + `); + }); + rect.addEventListener("mouseleave", hideTooltip); + }); + + // Draw MRR Line (Left Y-axis scale) + let mrrPathPoints = ""; + monthlyProjections.forEach((d, i) => { + const x = paddingLeft + (i / 12) * chartW; + const y = paddingTop + chartH - (d.endingMrr / yMaxLeft) * chartH; + mrrPathPoints += `${i === 0 ? "M" : "L"} ${x} ${y}`; + }); + + const mrrPath = document.createElementNS("http://www.w3.org/2000/svg", "path"); + mrrPath.setAttribute("d", mrrPathPoints); + mrrPath.setAttribute("class", "line-mrr"); + svgElement.appendChild(mrrPath); + + // Draw Line Dot circles + monthlyProjections.forEach((d, i) => { + const x = paddingLeft + (i / 12) * chartW; + const y = paddingTop + chartH - (d.endingMrr / yMaxLeft) * chartH; + + const dot = document.createElementNS("http://www.w3.org/2000/svg", "circle"); + dot.setAttribute("cx", x); + dot.setAttribute("cy", y); + dot.setAttribute("r", 4); + dot.setAttribute("fill", "#0ea5e9"); + dot.setAttribute("stroke", "#ffffff"); + dot.setAttribute("stroke-width", "1"); + dot.setAttribute("style", "cursor: pointer;"); + svgElement.appendChild(dot); + + dot.addEventListener("mouseenter", (e) => { + dot.setAttribute("r", 6); + showTooltip(e, ` + ${d.monthName} MRR
+ Starting: ${formatCurrency(d.startingMrr)}
+ New Added: +${formatCurrency(d.newMrr)}
+ Churn Loss: -${formatCurrency(d.churnedMrr)}
+ Ending MRR: ${formatCurrency(d.endingMrr)} + `); + }); + + dot.addEventListener("mouseleave", () => { + dot.setAttribute("r", 4); + hideTooltip(); + }); + }); +} + +// Tooltip positioning helper +function showTooltip(e, htmlContent) { + tooltip.innerHTML = htmlContent; + tooltip.style.display = "block"; + + const tooltipW = tooltip.offsetWidth; + const tooltipH = tooltip.offsetHeight; + + let x = e.pageX + 15; + let y = e.pageY - tooltipH - 10; + + if (x + tooltipW > window.innerWidth) { + x = e.pageX - tooltipW - 15; + } + if (y < window.scrollY) { + y = e.pageY + 20; + } + + tooltip.style.left = `${x}px`; + tooltip.style.top = `${y}px`; +} + +function hideTooltip() { + tooltip.style.display = "none"; +} + +// RENDER ADVISORY INTELLIGENCE ALERTS FEED +function renderRecommendations(ltvCac, payback, churn, arpa) { + recommendationsFeed.innerHTML = ""; + + // 1. Churn warnings + const churnCard = document.createElement("div"); + if (churn > 7) { + churnCard.className = "advisory-item adv-danger"; + churnCard.innerHTML = ` +
High Customer Churn
+

Your monthly churn rate is critical at ${churn}%. Churn is a compounding growth killer; at this rate, you cancel out new MRR inputs, leading to high revenue decay rates.

+ `; + } else if (churn <= 3) { + churnCard.className = "advisory-item adv-success"; + churnCard.innerHTML = ` +
Great Customer Retention
+

Healthy monthly churn of ${churn}%. Keeping churn under 3% indicates solid product-market fit and extends average customer lifespan.

+ `; + } else { + churnCard.className = "advisory-item adv-info"; + churnCard.innerHTML = ` +
Moderate Customer Retention
+

Standard SaaS churn rate of ${churn}%. Aim to reduce contract cancellations by optimizing user onboarding steps.

+ `; + } + recommendationsFeed.appendChild(churnCard); + + // 2. Unit economics advice LTV:CAC + const unitCard = document.createElement("div"); + if (ltvCac === Infinity) { + unitCard.className = "advisory-item adv-success"; + unitCard.innerHTML = ` +
Infinite Viability
+

With 0% churn, customer lifetime value is infinite. Model conservative churn metrics to find real-world limits.

+ `; + } else if (ltvCac < 1.5) { + unitCard.className = "advisory-item adv-danger"; + unitCard.innerHTML = ` +
Unstable Economics
+

Your LTV:CAC is ${ltvCac.toFixed(1)}x. A ratio under 1.5x means customer acquisition costs are too high relative to value. Focus on increasing ARPA or lowering CAC immediately.

+ `; + } else if (ltvCac < 3.0) { + unitCard.className = "advisory-item adv-warning"; + unitCard.innerHTML = ` +
Improve Margins
+

LTV:CAC is ${ltvCac.toFixed(1)}x. Try extending customer retention metrics or testing higher product pricing tiers to increase ARPA.

+ `; + } else { + unitCard.className = "advisory-item adv-success"; + unitCard.innerHTML = ` +
Scale Acquisitions
+

Excellent unit economics: LTV:CAC is ${ltvCac.toFixed(1)}x. Your acquisitions are highly profitable. Suggest scaling up marketing expenditures to capture market share.

+ `; + } + recommendationsFeed.appendChild(unitCard); + + // 3. CAC Payback advice + const paybackCard = document.createElement("div"); + if (payback > 18) { + paybackCard.className = "advisory-item adv-danger"; + paybackCard.innerHTML = ` +
Long Payback Period
+

CAC payback takes ${payback.toFixed(1)} months. Slow paybacks constrain cash flow, requiring external financing to fund new acquisition channels.

+ `; + } else if (payback <= 12) { + paybackCard.className = "advisory-item adv-success"; + paybackCard.innerHTML = ` +
Rapid Payback Pace
+

Payback period is only ${payback.toFixed(1)} months. Recovering customer acquisition costs under 12 months is top-quartile SaaS performance.

+ `; + } else { + paybackCard.className = "advisory-item adv-info"; + paybackCard.innerHTML = ` +
Standard Payback Pace
+

Recovering customer cost takes ${payback.toFixed(1)} months. Maintain operational focus on reducing sales friction.

+ `; + } + recommendationsFeed.appendChild(paybackCard); +} + +// SCENARIO VAULT SNAPSHOT LISTS +function renderScenarioList() { + scenarioList.innerHTML = ""; + + if (scenarios.length === 0) { + scenarioList.innerHTML = `
  • No saved KPI snapshots.
  • `; + return; + } + + scenarios.forEach(sc => { + const li = document.createElement("li"); + li.className = `scenario-item ${sc.id === activeScenarioId ? "active-scenario" : ""}`; + li.innerHTML = ` + ${escapeHtml(sc.name)} + + `; + + // Click load scenario details + li.addEventListener("click", (e) => { + if (e.target.tagName !== "I" && e.target.tagName !== "BUTTON") { + loadScenarioDetails(sc.id); + } + }); + + // Delete scenario snapshot + li.querySelector(".btn-delete-scenario").addEventListener("click", (e) => { + e.stopPropagation(); + deleteScenario(sc.id); + }); + + scenarioList.appendChild(li); + }); +} + +function loadScenarioDetails(id) { + const sc = scenarios.find(s => s.id === id); + if (!sc) return; + + activeScenarioId = id; + txtMrr.value = sc.mrr; + txtNewMrr.value = sc.newMrr; + txtArpa.value = sc.arpa; + txtCac.value = sc.cac; + sliderChurn.value = sc.churn; + lblChurnVal.textContent = `${sc.churn}%`; + + updateActiveScenarioHighlight(); + calculateAndRender(); +} + +function updateActiveScenarioHighlight() { + document.querySelectorAll(".scenario-item").forEach(item => { + item.classList.remove("active-scenario"); + }); + if (activeScenarioId) { + renderScenarioList(); + } +} + +function saveCurrentScenario() { + const name = txtScenarioName.value.trim(); + if (!name) { + alert("Please enter a Scenario Name first."); + return; + } + + const id = "sc-" + Date.now(); + const snapshot = { + id: id, + name: name, + mrr: Number(txtMrr.value) || 0, + newMrr: Number(txtNewMrr.value) || 0, + arpa: Number(txtArpa.value) || 1, + cac: Number(txtCac.value) || 1, + churn: Number(sliderChurn.value) || 0 + }; + + scenarios.push(snapshot); + txtScenarioName.value = ""; + activeScenarioId = id; + + saveToStorage(); + calculateAndRender(); +} + +function deleteScenario(id) { + if (confirm("Delete this saved KPI snapshot?")) { + scenarios = scenarios.filter(s => s.id !== id); + if (activeScenarioId === id) { + activeScenarioId = null; + } + saveToStorage(); + calculateAndRender(); + } +} + +function resetWorkspace() { + if (confirm("Restore KPI metrics inputs to default baseline plan?")) { + scenarios = [...DEFAULT_SCENARIOS]; + activeScenarioId = "sc-baseline"; + saveToStorage(); + loadScenarioDetails("sc-baseline"); + } +} + +// HELPERS +function formatCurrency(amount) { + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + maximumFractionDigits: 0 + }).format(amount); +} + +function formatCurrencyShort(amount) { + if (amount >= 1000000) { + return `$${(amount / 1000000).toFixed(1)}M`; + } else if (amount >= 1000) { + return `$${(amount / 1000).toFixed(0)}k`; + } + return `$${amount}`; +} + +function formatNumber(num) { + return new Intl.NumberFormat("en-US", { maximumFractionDigits: 0 }).format(num); +} + +function formatNumberShort(num) { + if (num >= 1000000) { + return (num / 1000000).toFixed(1) + "M"; + } else if (num >= 1000) { + return (num / 1000).toFixed(0) + "k"; + } + return num.toFixed(0); +} + +function escapeHtml(str) { + if (!str) return ""; + return str + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} diff --git a/Projects/Startup KPI Dashboard/style.css b/Projects/Startup KPI Dashboard/style.css new file mode 100644 index 0000000..00effdd --- /dev/null +++ b/Projects/Startup KPI Dashboard/style.css @@ -0,0 +1,743 @@ +/* Startup KPI Dashboard - Styling */ + +:root { + --bg-main: #090d16; + --bg-card: #111827; + --bg-card-hover: #1f2937; + --border-color: #374151; + --text-main: #f3f4f6; + --text-muted: #9ca3af; + --text-highlight: #ffffff; + + /* Accent theme color palettes */ + --clr-primary: #0ea5e9; + --clr-primary-glow: rgba(14, 165, 233, 0.15); + + --clr-emerald: #10b981; + --clr-emerald-glow: rgba(16, 185, 129, 0.15); + + --clr-amber: #f59e0b; + --clr-amber-glow: rgba(245, 158, 11, 0.15); + + --clr-rose: #ef4444; + --clr-rose-glow: rgba(239, 68, 68, 0.15); + + --font-headers: 'Outfit', sans-serif; + --font-body: 'Inter', sans-serif; + + --transition-speed: 0.25s; + --radius-sm: 8px; + --radius-md: 12px; + --radius-lg: 16px; + --glow-shadow: 0 0 20px rgba(14, 165, 233, 0.25); +} + +/* GENERAL STYLES */ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + background-color: var(--bg-main); + color: var(--text-main); + font-family: var(--font-body); + min-height: 100vh; + display: flex; + flex-direction: column; + position: relative; + overflow-x: hidden; + line-height: 1.5; +} + +/* GLOW BACKGROUND */ +.glow-bg { + position: absolute; + top: -150px; + left: 50%; + transform: translateX(-50%); + width: 800px; + height: 400px; + background: radial-gradient(circle, rgba(14, 165, 233, 0.1) 0%, rgba(16, 185, 129, 0.05) 50%, rgba(9, 13, 22, 0) 100%); + filter: blur(80px); + z-index: -1; + pointer-events: none; +} + +/* SCROLLBARS */ +::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.1); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb { + background: var(--border-color); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--text-muted); +} + +/* HEADER */ +.app-header { + background: rgba(17, 24, 39, 0.7); + backdrop-filter: blur(12px); + border-bottom: 1px solid var(--border-color); + padding: 1rem 2rem; + position: sticky; + top: 0; + z-index: 100; +} + +.header-container { + max-width: 1600px; + margin: 0 auto; + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 1rem; +} + +.logo { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.logo h1 { + font-family: var(--font-headers); + font-size: 1.75rem; + font-weight: 800; + letter-spacing: -0.5px; + background: linear-gradient(135deg, #ffffff 30%, var(--clr-primary) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.logo h1 span { + color: var(--text-muted); + -webkit-text-fill-color: var(--text-muted); + font-weight: 400; +} + +.glow-icon { + font-size: 1.5rem; + color: var(--clr-primary); + text-shadow: 0 0 10px rgba(14, 165, 233, 0.5); + animation: pulse 3s infinite alternate; +} + +.logo .badge { + font-size: 0.75rem; + font-weight: 600; + padding: 0.25rem 0.6rem; + background: rgba(31, 41, 55, 0.6); + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 1px; +} + +.header-stats { + display: flex; + gap: 1rem; +} + +.status-indicator { + display: inline-flex; + align-items: center; + padding: 0.4rem 0.8rem; + border-radius: var(--radius-sm); + font-size: 0.9rem; + font-weight: 700; + letter-spacing: 0.5px; +} + +.status-green { + background: var(--clr-emerald-glow); + color: #6ee7b7; + border: 1px solid var(--clr-emerald); +} + +.status-amber { + background: var(--clr-amber-glow); + color: #fde047; + border: 1px solid var(--clr-amber); +} + +.status-red { + background: var(--clr-rose-glow); + color: #fca5a5; + border: 1px solid var(--clr-rose); +} + +/* MAIN LAYOUT */ +.main-container { + display: grid; + grid-template-columns: 300px 1fr 300px; + gap: 1.5rem; + max-width: 1600px; + width: 100%; + margin: 0 auto; + padding: 1.5rem 2rem; + flex: 1; +} + +/* SECTION CARDS */ +.section-card { + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + padding: 1.25rem; + margin-bottom: 1.25rem; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + transition: border-color var(--transition-speed); +} + +.section-card:hover { + border-color: rgba(255, 255, 255, 0.08); +} + +.card-header-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + padding-bottom: 0.5rem; +} + +.card-header-row h3 { + font-family: var(--font-headers); + font-size: 1.1rem; + font-weight: 600; + display: flex; + align-items: center; + gap: 0.5rem; + color: var(--text-highlight); +} + +/* FORM GROUPS */ +.form-group { + margin-bottom: 1rem; +} + +.form-group label { + display: block; + font-size: 0.8rem; + font-weight: 500; + text-transform: uppercase; + color: var(--text-muted); + margin-bottom: 0.4rem; + letter-spacing: 0.5px; +} + +input[type="text"], +input[type="number"] { + width: 100%; + padding: 0.65rem 0.9rem; + background-color: rgba(9, 13, 22, 0.6); + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + color: var(--text-main); + font-family: var(--font-body); + font-size: 0.9rem; + transition: border-color var(--transition-speed), box-shadow var(--transition-speed); +} + +input[type="text"]:focus, +input[type="number"]:focus { + outline: none; + border-color: var(--clr-primary); + box-shadow: 0 0 0 2px rgba(14, 165, 233, 0.2); +} + +.form-row { + display: flex; + gap: 1rem; +} + +.flex-1 { + flex: 1; +} + +/* RANGE SLIDER */ +.slider-label-row { + display: flex; + justify-content: space-between; + align-items: center; +} + +.slider-val { + font-weight: 700; + color: var(--clr-primary); + font-size: 0.9rem; +} + +input[type="range"] { + -webkit-appearance: none; + width: 100%; + background: transparent; + margin: 0.5rem 0; +} + +input[type="range"]:focus { + outline: none; +} + +input[type="range"]::-webkit-slider-runnable-track { + width: 100%; + height: 6px; + cursor: pointer; + background: var(--border-color); + border-radius: 3px; +} + +input[type="range"]::-webkit-slider-thumb { + height: 16px; + width: 16px; + border-radius: 50%; + background: var(--clr-primary); + cursor: pointer; + -webkit-appearance: none; + margin-top: -5px; + box-shadow: 0 0 10px rgba(14, 165, 233, 0.5); + transition: transform 0.1s; +} + +input[type="range"]::-webkit-slider-thumb:hover { + transform: scale(1.2); +} + +/* BUTTONS */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.65rem 1.25rem; + border-radius: var(--radius-sm); + font-family: var(--font-body); + font-weight: 600; + font-size: 0.9rem; + cursor: pointer; + transition: all var(--transition-speed); + border: 1px solid transparent; + width: 100%; +} + +.btn-sm { + padding: 0.45rem 0.9rem; + font-size: 0.8rem; +} + +.btn-primary { + background: linear-gradient(135deg, var(--clr-primary) 0%, #0284c7 100%); + color: white; + box-shadow: var(--glow-shadow); +} + +.btn-primary:hover { + transform: translateY(-1px); + filter: brightness(1.1); + box-shadow: 0 0 25px rgba(14, 165, 233, 0.4); +} + +.footer-btn { + background: transparent; + border: 1px dashed var(--border-color); + color: var(--text-muted); + padding: 0.75rem; + border-radius: var(--radius-sm); + cursor: pointer; + transition: all var(--transition-speed); + font-family: var(--font-body); + font-size: 0.85rem; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + width: 100%; +} + +.footer-btn:hover { + border-color: var(--clr-rose); + color: var(--clr-rose); + background: rgba(239, 68, 68, 0.05); +} + +/* SIDEBAR COLUMN (LEFT / RIGHT) */ +.sidebar-column { + display: flex; + flex-direction: column; +} + +/* CENTER COLUMN METRICS */ +.chart-column { + display: flex; + flex-direction: column; +} + +.metrics-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 0.75rem; + margin-bottom: 1.25rem; +} + +.metric-card { + background: var(--bg-card); + border: 1px solid var(--border-color); + padding: 1rem 0.75rem; + border-radius: var(--radius-md); + display: flex; + flex-direction: column; + gap: 0.25rem; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.m-lbl { + font-size: 0.7rem; + color: var(--text-muted); + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.m-val { + font-family: var(--font-headers); + font-size: 1.5rem; + font-weight: 800; + color: var(--text-highlight); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.m-sub { + font-size: 0.7rem; + color: var(--text-muted); +} + +.text-cyan { color: #22d3ee; } +.text-amber { color: #fde047; } +.text-emerald { color: #6ee7b7; } +.text-rose { color: #fca5a5; } + +/* SVG CHART CONTAINER */ +.chart-legend { + display: flex; + gap: 1rem; + font-size: 0.75rem; +} + +.legend-item { + display: flex; + align-items: center; + gap: 0.35rem; + color: var(--text-muted); +} + +.legend-color-dot { + width: 8px; + height: 8px; + border-radius: 50%; +} + +.dot-cyan { background-color: var(--clr-primary); } +.dot-emerald { background-color: var(--clr-emerald); } + +.chart-container { + width: 100%; + height: 300px; + margin-top: 1rem; + position: relative; +} + +#growth-chart-svg { + width: 100%; + height: 100%; + display: block; +} + +/* CHART SVG PATHS AND RECTS */ +.grid-line { + stroke: rgba(255, 255, 255, 0.04); + stroke-width: 1; +} + +.axis-line { + stroke: var(--border-color); + stroke-width: 1.5; +} + +.axis-label { + fill: var(--text-muted); + font-size: 10px; + font-family: var(--font-body); +} + +.chart-bar { + fill: var(--clr-emerald-glow); + stroke: var(--clr-emerald); + stroke-width: 1; + opacity: 0.85; + transition: opacity 0.2s, fill 0.2s; + cursor: pointer; +} + +.chart-bar:hover { + opacity: 1; + fill: rgba(16, 185, 129, 0.3); +} + +.line-mrr { + fill: none; + stroke: var(--clr-primary); + stroke-width: 3; + stroke-linecap: round; + stroke-linejoin: round; + filter: drop-shadow(0px 0px 6px rgba(14, 165, 233, 0.3)); +} + +.chart-tooltip { + position: absolute; + background: rgba(17, 24, 39, 0.95); + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + padding: 0.5rem; + font-size: 0.75rem; + color: var(--text-main); + pointer-events: none; + box-shadow: 0 4px 10px rgba(0,0,0,0.5); + z-index: 10; + display: none; +} + +/* DATA TABLE */ +.table-card { + margin-top: 1.25rem; +} + +.table-wrapper { + max-height: 250px; + overflow-y: auto; + border-radius: var(--radius-sm); + border: 1px solid var(--border-color); +} + +.forecast-table { + width: 100%; + border-collapse: collapse; + font-size: 0.8rem; + text-align: left; +} + +.forecast-table th, +.forecast-table td { + padding: 0.6rem 0.8rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.03); +} + +.forecast-table th { + background: rgba(17, 24, 39, 0.9); + color: var(--text-muted); + font-weight: 600; + position: sticky; + top: 0; + z-index: 1; +} + +.forecast-table tbody tr:hover { + background: rgba(255, 255, 255, 0.02); +} + +/* SCENARIO SNAPSHOT VAULT */ +.scenario-save-form { + padding: 0.75rem; + background: rgba(9, 13, 22, 0.4); + border: 1px solid rgba(255, 255, 255, 0.02); + border-radius: var(--radius-sm); + margin-bottom: 1rem; +} + +.saved-scenarios-section h5 { + font-family: var(--font-headers); + font-size: 0.85rem; + font-weight: 700; + text-transform: uppercase; + color: var(--text-muted); + margin-bottom: 0.5rem; +} + +.scenario-ul { + list-style: none; + display: flex; + flex-direction: column; + gap: 0.5rem; + max-height: 180px; + overflow-y: auto; +} + +.scenario-item { + display: flex; + align-items: center; + justify-content: space-between; + background: rgba(9, 13, 22, 0.6); + border: 1px solid var(--border-color); + padding: 0.4rem 0.6rem; + border-radius: var(--radius-sm); + font-size: 0.8rem; + cursor: pointer; + transition: all var(--transition-speed); +} + +.scenario-item:hover { + border-color: var(--clr-primary); + background: rgba(17, 24, 39, 0.8); +} + +.scenario-item.active-scenario { + border-color: var(--clr-emerald); + background: rgba(16, 185, 129, 0.05); +} + +.scenario-name { + font-weight: 600; + color: var(--text-highlight); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 180px; +} + +.btn-delete-scenario { + background: transparent; + border: none; + color: var(--text-muted); + cursor: pointer; + padding: 0.2rem; + transition: color var(--transition-speed); +} + +.btn-delete-scenario:hover { + color: var(--clr-rose); +} + +/* ADVISORY FEED */ +.advisory-card { + flex: 1; +} + +.advisory-feed { + display: flex; + flex-direction: column; + gap: 0.75rem; + max-height: 300px; + overflow-y: auto; +} + +.advisory-item { + padding: 0.75rem; + border-radius: var(--radius-sm); + font-size: 0.8rem; + border-left: 4px solid transparent; + background: rgba(9, 13, 22, 0.4); +} + +.adv-success { + border-left-color: var(--clr-emerald); + background: rgba(16, 185, 129, 0.05); + color: #a7f3d0; +} + +.adv-warning { + border-left-color: var(--clr-amber); + background: rgba(245, 158, 11, 0.05); + color: #fef08a; +} + +.adv-danger { + border-left-color: var(--clr-rose); + background: rgba(239, 68, 68, 0.05); + color: #fecaca; +} + +.adv-info { + border-left-color: var(--clr-primary); + background: rgba(14, 165, 233, 0.05); + color: #bae6fd; +} + +.advisory-title { + font-weight: 700; + display: flex; + align-items: center; + gap: 0.4rem; + margin-bottom: 0.25rem; + font-size: 0.85rem; +} + +/* FOOTER */ +.app-footer { + border-top: 1px solid var(--border-color); + padding: 1.5rem 2rem; + text-align: center; + background-color: rgba(9, 13, 22, 0.8); + font-size: 0.85rem; + color: var(--text-muted); + margin-top: auto; +} + +.footer-container a { + color: var(--clr-primary); + text-decoration: none; + transition: color var(--transition-speed); +} + +.footer-container a:hover { + color: #38bdf8; + text-decoration: underline; +} + +/* ANIMATIONS */ +@keyframes pulse { + 0% { text-shadow: 0 0 8px rgba(14, 165, 233, 0.3); } + 100% { text-shadow: 0 0 16px rgba(14, 165, 233, 0.7); } +} + +/* RESPONSIVE LAYOUTS */ +@media (max-width: 1200px) { + .main-container { + grid-template-columns: 280px 1fr; + } + .advisory-card, .scenario-card { + grid-column: span 1; + } +} + +@media (max-width: 900px) { + .main-container { + grid-template-columns: 1fr; + padding: 1rem; + } + .sidebar-column, .chart-column { + grid-column: span 1; + } + .metrics-grid { + grid-template-columns: repeat(2, 1fr); + } + .header-container { + flex-direction: column; + align-items: flex-start; + } +} diff --git a/Projects/Startup KPI Dashboard/thumbnail.svg b/Projects/Startup KPI Dashboard/thumbnail.svg new file mode 100644 index 0000000..65d075e --- /dev/null +++ b/Projects/Startup KPI Dashboard/thumbnail.svg @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + KPISyncPro + + + METRICS + + + + + + + $600,000 ARR + + + + + + + LTV : CAC RATIO + 3.3x + + + + LIFETIME VALUE + $2,000 + + + + CAC PAYBACK + 6.0 mo + + + + MONTHLY CHURN + 5.0% + + + + + + MRR & Customers Growth + + + + MRR + + Users + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Base + Month 4 + Month 8 + Month 12 + + + + + + KPI Vault + + + + ★ SaaS Baseline + + + + Product-Led + + + + + Enterprise + + + + + + + 💡 CAC Payback ok. + + +