Реалізація онлайн-калькулятора на сайті

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація онлайн-калькулятора на сайті
Середня
~3-5 робочих днів
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Реалізація онлайн-калькулятора на вебсайті

Онлайн-калькулятор — один з найбільш конвертуючих інструментів на сайті. Іпотечний калькулятор, розрахунок вартості послуг, калькулятор ROI — усі вони будуються за однією архітектурою, але диявол в деталях: формули, валідація, форматування чисел, анімація результату.

Архітектура: формула як конфіг

Неправильний підхід — зашити формули прямо в обробники подій. Правильний — відокремити логіку розрахунку від UI:

// types.ts
interface CalculatorField {
  id: string
  label: string
  type: 'number' | 'range' | 'select' | 'radio'
  min?: number
  max?: number
  step?: number
  defaultValue: number
  unit?: string
  options?: { label: string; value: number }[]
  format?: 'currency' | 'percent' | 'number'
}

interface CalculatorConfig {
  id: string
  fields: CalculatorField[]
  formula: (inputs: Record<string, number>) => CalculatorResult
  resultFields: ResultField[]
}

interface CalculatorResult {
  [key: string]: number
}

Іпотечний калькулятор: реалізація

// mortgage-calculator.ts
export const mortgageCalculator: CalculatorConfig = {
  id: 'mortgage',
  fields: [
    { id: 'price', label: 'Вартість нерухомості', type: 'number',
      min: 500_000, max: 100_000_000, step: 100_000, defaultValue: 5_000_000,
      format: 'currency' },
    { id: 'downPayment', label: 'Перший внесок', type: 'range',
      min: 10, max: 90, step: 1, defaultValue: 20, unit: '%', format: 'percent' },
    { id: 'rate', label: 'Процентна ставка', type: 'number',
      min: 0.1, max: 30, step: 0.1, defaultValue: 11.5, unit: '% на рік', format: 'percent' },
    { id: 'term', label: 'Термін кредиту', type: 'select',
      defaultValue: 20,
      options: [5, 10, 15, 20, 25, 30].map(y => ({ label: `${y} років`, value: y })) },
  ],
  formula: ({ price, downPayment, rate, term }) => {
    const principal = price * (1 - downPayment / 100)
    const monthlyRate = rate / 100 / 12
    const months = term * 12

    // Формула ануїтетного платежу
    const payment = monthlyRate === 0
      ? principal / months
      : principal * (monthlyRate * Math.pow(1 + monthlyRate, months))
          / (Math.pow(1 + monthlyRate, months) - 1)

    const totalPayment = payment * months
    const overpayment = totalPayment - principal

    return { payment, totalPayment, overpayment, principal }
  },
  resultFields: [
    { id: 'payment', label: 'Щомісячний платіж', format: 'currency', highlight: true },
    { id: 'totalPayment', label: 'Загальна сума виплат', format: 'currency' },
    { id: 'overpayment', label: 'Переплата', format: 'currency' },
    { id: 'principal', label: 'Сума кредиту', format: 'currency' },
  ],
}

React-компонент калькулятора

import { useState, useCallback, useMemo } from 'react'

function formatValue(value: number, format?: string): string {
  switch (format) {
    case 'currency':
      return new Intl.NumberFormat('uk-UA', {
        style: 'currency',
        currency: 'UAH',
        maximumFractionDigits: 0,
      }).format(value)
    case 'percent':
      return `${value.toFixed(1)}%`
    default:
      return new Intl.NumberFormat('uk-UA').format(value)
  }
}

export function Calculator({ config }: { config: CalculatorConfig }) {
  const [values, setValues] = useState<Record<string, number>>(
    Object.fromEntries(config.fields.map(f => [f.id, f.defaultValue]))
  )

  const result = useMemo(() => {
    try {
      return config.formula(values)
    } catch {
      return null
    }
  }, [values, config])

  const handleChange = useCallback((id: string, value: number) => {
    setValues(prev => ({ ...prev, [id]: value }))
  }, [])

  return (
    <div className="calculator">
      <div className="calculator__inputs">
        {config.fields.map(field => (
          <CalculatorField
            key={field.id}
            field={field}
            value={values[field.id]}
            onChange={val => handleChange(field.id, val)}
          />
        ))}
      </div>
      {result && (
        <div className="calculator__results">
          {config.resultFields.map(rf => (
            <div key={rf.id} className={`result-item ${rf.highlight ? 'result-item--highlight' : ''}`}>
              <span className="result-item__label">{rf.label}</span>
              <AnimatedNumber
                value={result[rf.id]}
                format={rf.format}
              />
            </div>
          ))}
        </div>
      )}
    </div>
  )
}

Анімація зміни числа

import { useEffect, useRef, useState } from 'react'

function AnimatedNumber({ value, format }: { value: number; format?: string }) {
  const [displayValue, setDisplayValue] = useState(value)
  const animationRef = useRef<number>()
  const startRef = useRef(value)
  const startTimeRef = useRef<number>()

  useEffect(() => {
    const startValue = displayValue
    startRef.current = startValue
    startTimeRef.current = undefined

    const duration = 400  // ms

    const animate = (timestamp: number) => {
      if (!startTimeRef.current) startTimeRef.current = timestamp
      const elapsed = timestamp - startTimeRef.current
      const progress = Math.min(elapsed / duration, 1)

      // Easing: ease-out
      const eased = 1 - Math.pow(1 - progress, 3)
      const current = startValue + (value - startValue) * eased

      setDisplayValue(current)

      if (progress < 1) {
        animationRef.current = requestAnimationFrame(animate)
      }
    }

    animationRef.current = requestAnimationFrame(animate)
    return () => { if (animationRef.current) cancelAnimationFrame(animationRef.current) }
  }, [value])

  return <span className="animated-number">{formatValue(displayValue, format)}</span>
}

Валідація введення

function CalculatorField({ field, value, onChange }: FieldProps) {
  const [rawValue, setRawValue] = useState(String(value))
  const [error, setError] = useState('')

  function handleInput(e: React.ChangeEvent<HTMLInputElement>) {
    const raw = e.target.value
    setRawValue(raw)

    const num = parseFloat(raw.replace(/\s/g, '').replace(',', '.'))

    if (isNaN(num)) {
      setError('Введіть число')
      return
    }

    if (field.min !== undefined && num < field.min) {
      setError(`Мінімум: ${formatValue(field.min, field.format)}`)
      return
    }

    if (field.max !== undefined && num > field.max) {
      setError(`Максимум: ${formatValue(field.max, field.format)}`)
      return
    }

    setError('')
    onChange(num)
  }

  // Синхронізація при зовнішній зміні (наприклад, від range slider)
  useEffect(() => {
    setRawValue(String(value))
    setError('')
  }, [value])

  return (
    <div className={`field ${error ? 'field--error' : ''}`}>
      <label htmlFor={field.id}>{field.label}</label>
      <input
        id={field.id}
        type="text"
        inputMode="decimal"
        value={rawValue}
        onChange={handleInput}
      />
      {field.unit && <span className="field__unit">{field.unit}</span>}
      {error && <span className="field__error">{error}</span>}
    </div>
  )
}

Збереження результату та поділ

// Кодуємо параметри в URL для поділу
function encodeCalcState(values: Record<string, number>): string {
  const params = new URLSearchParams(
    Object.entries(values).map(([k, v]) => [k, String(v)])
  )
  return params.toString()
}

function decodeCalcState(search: string): Record<string, number> {
  const params = new URLSearchParams(search)
  const result: Record<string, number> = {}
  params.forEach((v, k) => { result[k] = parseFloat(v) })
  return result
}

// При зміні значень оновлюємо URL без перезавантаження
function syncToUrl(values: Record<string, number>) {
  const url = new URL(window.location.href)
  url.search = encodeCalcState(values)
  window.history.replaceState({}, '', url.toString())
}

Діаграма розподілу платежів

// Проста pie chart через SVG без бібліотек
function PaymentPieChart({ principal, overpayment }: { principal: number; overpayment: number }) {
  const total = principal + overpayment
  const principalPct = principal / total
  const angle = principalPct * 360

  // SVG arc path
  const r = 80
  const cx = 100, cy = 100
  const rad = (deg: number) => (deg - 90) * Math.PI / 180
  const x = (deg: number) => cx + r * Math.cos(rad(deg))
  const y = (deg: number) => cy + r * Math.sin(rad(deg))

  const largeArc = angle > 180 ? 1 : 0
  const path1 = `M ${cx} ${cy} L ${x(0)} ${y(0)} A ${r} ${r} 0 ${largeArc} 1 ${x(angle)} ${y(angle)} Z`
  const path2 = `M ${cx} ${cy} L ${x(angle)} ${y(angle)} A ${r} ${r} 0 ${1 - largeArc} 1 ${x(360)} ${y(360)} Z`

  return (
    <svg viewBox="0 0 200 200" width="200" height="200">
      <path d={path1} fill="#6366f1" />
      <path d={path2} fill="#f43f5e" />
    </svg>
  )
}

Терміни

Простий калькулятор з 3–5 полями та одним результатом — 1 день. Іпотечний калькулятор з анімацією, діаграмою, URL-поділом та адаптивним макетом — 2–3 дні. Конфігурована система з кількома калькуляторами та управлінням через CMS — 1 тиждень.