'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 (
{label} {priceLabel}
onChange(Math.max(min, Math.min(max, Number(e.target.value) || 0)))} className={`w-16 text-center font-joey-medium text-fd-body rounded-full px-2 py-1.5 border-2 ${inputColor} [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none`} style={{ appearance: 'textfield' }} /> {unit}
) } // ─── 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 */}
) }