'use client'
import React, { useState, useMemo } from 'react'
import type { FDVpsCalculatorBlock as FDVpsCalculatorBlockProps } from '@/payload-types'
const DEFAULT_PRICING = {
windows: 250,
cpuPerCore: 120,
ramPerGb: 100,
ssdPerGb: 4,
hddPerGb: 1,
adminFee: 200,
}
const formatKr = (n: number) => {
// Format like "2 495 kr" with space as thousands separator
return Math.round(n).toLocaleString('sv-SE') + ' kr'
}
// ─── Toggle switch ───────────────────────────────────────────────────────────
function Toggle({ active, onToggle }: { active: boolean; onToggle: () => void }) {
return (
)
}
// ─── Number stepper row ──────────────────────────────────────────────────────
function ResourceRow({
label,
value,
onChange,
min = 0,
max = 999,
step = 1,
unit,
priceLabel,
isDark,
}: {
label: string
value: number
onChange: (v: number) => void
min?: number
max?: number
step?: number
unit: string
priceLabel: string
isDark: boolean
}) {
const borderColor = isDark ? 'rgba(255,255,255,0.1)' : 'rgba(14,35,56,0.08)'
const labelColor = isDark ? 'text-white' : 'text-fd-navy'
const subColor = isDark ? 'text-white/50' : 'text-fd-navy/50'
const btnColor = isDark
? 'border-white/30 text-white hover:border-white/70'
: 'border-fd-navy/20 text-fd-navy hover:border-fd-navy/60'
const inputColor = isDark
? 'bg-white/10 border-white/20 text-white'
: 'bg-fd-surface-alt border-fd-navy/15 text-fd-navy'
const unitColor = isDark ? 'text-white/40' : 'text-fd-navy/40'
return (
)
}
// ─── Main component ──────────────────────────────────────────────────────────
export const FDVpsCalculatorBlockComponent: React.FC = ({
heading = 'Virtuell server — kalkylator',
description,
contactCtaText = 'Frågor? Kontakta oss',
contactCtaLink = '/kontakt',
orderCtaText = 'Beställ',
orderCtaLink = '/kontakt?subject=vps-bestallning',
sectionBackground = 'white',
pricingCpuPerCore,
pricingRamPerGb,
pricingSsdPerGb,
pricingHddPerGb,
pricingWindowsLicense,
discountPercent,
showAdminFee,
adminFeeAmount,
additionalServices = [],
}) => {
const pricing = {
windows: pricingWindowsLicense ?? DEFAULT_PRICING.windows,
cpuPerCore: pricingCpuPerCore ?? DEFAULT_PRICING.cpuPerCore,
ramPerGb: pricingRamPerGb ?? DEFAULT_PRICING.ramPerGb,
ssdPerGb: pricingSsdPerGb ?? DEFAULT_PRICING.ssdPerGb,
hddPerGb: pricingHddPerGb ?? DEFAULT_PRICING.hddPerGb,
}
const feeAmount = adminFeeAmount ?? DEFAULT_PRICING.adminFee
const discount = (discountPercent ?? 0) / 100
const [os, setOs] = useState<'linux' | 'windows'>('linux')
const [cpuCores, setCpuCores] = useState(2)
const [ramGb, setRamGb] = useState(4)
const [ssdGb, setSsdGb] = useState(50)
const [hddGb, setHddGb] = useState(0)
const [extraToggles, setExtraToggles] = useState>({})
const toggleExtra = (i: number) => setExtraToggles((p) => ({ ...p, [i]: !p[i] }))
const costs = useMemo(() => {
const disc = (v: number) => v * (1 - discount)
const licenseCost = os === 'windows' ? disc(pricing.windows) : 0
const cpuCost = disc(cpuCores * pricing.cpuPerCore)
const ramCost = disc(ramGb * pricing.ramPerGb)
const ssdCost = disc(ssdGb * pricing.ssdPerGb)
const hddCost = disc(hddGb * pricing.hddPerGb)
// Admin fee is fixed — always on if enabled in CMS, not customer-controlled
const feeCost = showAdminFee ? feeAmount : 0
const extraCosts = (additionalServices ?? []).map((svc, i) =>
extraToggles[i] ? (svc.price ?? 0) : 0
)
const extraTotal = extraCosts.reduce((a, b) => a + b, 0)
const total = licenseCost + cpuCost + ramCost + ssdCost + hddCost + feeCost + extraTotal
return { licenseCost, cpuCost, ramCost, ssdCost, hddCost, feeCost, extraCosts, total }
}, [os, cpuCores, ramGb, ssdGb, hddGb, extraToggles, pricing, discount, feeAmount, showAdminFee, additionalServices])
const isDark = sectionBackground === 'navy'
const bgClass = isDark ? 'bg-fd-navy' : sectionBackground === 'gray' ? 'bg-fd-surface-alt' : 'bg-white'
// Card styling
const cardBg = isDark ? 'rgba(255,255,255,0.05)' : '#ffffff'
const cardBorder = isDark ? 'rgba(255,255,255,0.12)' : '#e2e8f0'
const cardStyle: React.CSSProperties = {
background: cardBg,
border: `6px solid ${cardBorder}`,
borderRadius: 'clamp(28px, 4vw, 60px)',
}
// Colors
const headingColor = isDark ? 'text-white' : 'text-fd-navy'
const descColor = isDark ? 'text-white/60' : 'text-fd-navy/60'
const sectionLabelColor = isDark ? 'text-white/40' : 'text-fd-navy/35'
const dividerColor = isDark ? 'rgba(255,255,255,0.1)' : 'rgba(14,35,56,0.08)'
const summaryLabelColor = isDark ? 'text-white' : 'text-fd-navy'
const summaryValueColor = isDark ? 'text-white' : 'text-fd-navy'
const categoryLabelColor = isDark ? 'text-white/40' : 'text-fd-navy/40'
// OS toggle
const osActiveClass = 'bg-fd-yellow text-fd-navy border-fd-yellow font-joey-bold'
const osInactiveClass = isDark
? 'bg-transparent text-white/70 border-white/20 hover:border-white/50 font-joey'
: 'bg-transparent text-fd-navy/70 border-fd-navy/20 hover:border-fd-navy/50 font-joey'
const discStr = discount > 0 ? ` (${discountPercent}% rabatt)` : ''
// Summary rows
const baseRows = [
...(os === 'windows' ? [{ label: 'Licens (Windows)', cost: costs.licenseCost }] : []),
{ label: `CPU (${cpuCores} ${cpuCores === 1 ? 'kärna' : 'kärnor'})`, cost: costs.cpuCost },
{ label: `RAM (${ramGb} GB)`, cost: costs.ramCost },
{ label: `SSD NVMe (${ssdGb} GB)`, cost: costs.ssdCost },
...(hddGb > 0 ? [{ label: `HDD (${hddGb} GB)`, cost: costs.hddCost }] : []),
...(showAdminFee ? [{ label: 'Adminavgift', cost: costs.feeCost }] : []),
]
const tillvalRows = (additionalServices ?? []).flatMap((svc, i) =>
extraToggles[i] ? [{ label: svc.label ?? 'Tilläggstjänst', cost: costs.extraCosts[i] ?? 0 }] : []
)
const hasTillval = (additionalServices ?? []).length > 0
return (
{/* Section heading */}
{(heading || description) && (
{heading && (
{heading}
)}
{description && (
{description}
)}
{discount > 0 && (
{discountPercent}% rabatt på alla resurser
)}
)}
{/* ── Left: Config ── */}
{/* OS */}
Operativsystem
{(['linux', 'windows'] as const).map((opt) => (
))}
{/* Resources */}
{/* Admin fee — fixed line, not customer-controlled */}
{showAdminFee && (
Adminavgift
{formatKr(feeAmount)}
)}
{/* Tillvalstjänster */}
{hasTillval && (
Tillvalstjänster
{(additionalServices ?? []).map((svc, i) => (
{svc.label}
{svc.price != null && (
{svc.price} kr/mån
)}
toggleExtra(i)} />
))}
)}
{/* ── Right: Summary ── */}
Kostnadsöversikt
{/* Base cost rows */}
{baseRows.map((row, i) => (
{row.label}
{formatKr(row.cost)}
))}
{/* Tillval section in summary */}
{tillvalRows.length > 0 && (
<>
Tillval
{tillvalRows.map((row, i) => (
{row.label}
{formatKr(row.cost)}
))}
>
)}
{/* Total */}
Totalt per månad
{formatKr(costs.total)}
exkl. moms
{/* CTAs */}
)
}