wwwlayermeshusa/src/blocks/FDVpsCalculatorBlock/Component.tsx

275 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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) => Math.round(n).toLocaleString('sv-SE') + ' kr'
function Toggle({ active, onToggle }: { active: boolean; onToggle: () => void }) {
return (
<button
type="button"
onClick={onToggle}
style={{ display: 'block', width: '3.5rem', height: '2rem', flexShrink: 0 }}
className={`relative rounded-full transition-all duration-300 ${active ? 'bg-fd-yellow' : 'bg-[#e2e8f0] dark:bg-white/20'}`}
role="switch"
aria-checked={active}
>
<div className={`absolute top-1 w-6 h-6 rounded-full bg-white shadow-sm transition-all duration-300 ${active ? 'left-7' : 'left-1'}`} />
</button>
)
}
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
}) {
return (
<div className="flex items-center justify-between py-4 border-b border-fd-navy/10 dark:border-white/10">
<div className="flex flex-col">
<span className="font-joey-bold text-fd-body-lg text-fd-navy dark:text-white">{label}</span>
<span className="font-joey text-fd-body text-fd-navy/50 dark:text-white/50">{priceLabel}</span>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => onChange(Math.max(min, value - step))}
className="w-11 h-11 rounded-full border-2 flex items-center justify-center font-joey-bold text-lg transition-colors border-fd-navy/20 text-fd-navy hover:border-fd-navy/60 dark:border-white/30 dark:text-white dark:hover:border-white/70"
></button>
<input
type="number"
value={value}
onChange={(e) => onChange(Math.max(min, Math.min(max, Number(e.target.value) || 0)))}
className="w-20 text-center font-joey-medium text-fd-body-lg rounded-full px-2 py-1.5 border-2 bg-fd-surface-alt border-fd-navy/15 text-fd-navy dark:bg-white/10 dark:border-white/20 dark:text-white [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
style={{ appearance: 'textfield' }}
/>
<span className="font-joey text-fd-body w-8 text-fd-navy/40 dark:text-white/40">{unit}</span>
<button
type="button"
onClick={() => onChange(Math.min(max, value + step))}
className="w-11 h-11 rounded-full border-2 flex items-center justify-center font-joey-bold text-lg transition-colors border-fd-navy/20 text-fd-navy hover:border-fd-navy/60 dark:border-white/30 dark:text-white dark:hover:border-white/70"
>+</button>
</div>
</div>
)
}
export const FDVpsCalculatorBlockComponent: React.FC<FDVpsCalculatorBlockProps> = ({
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 = [],
anchorId,
}) => {
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<Record<number, boolean>>({})
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)
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 dark:bg-fd-navy' : 'bg-white dark:bg-fd-navy'
// Use Tailwind classes for card styling instead of inline styles
const cardClass = isDark
? 'bg-white/5 border-[5px] border-white/10 rounded-[32px] md:rounded-[50px] lg:rounded-[70px]'
: 'bg-white border-[5px] border-[#e2e8f0] rounded-[32px] md:rounded-[50px] lg:rounded-[70px] dark:bg-white/5 dark:border-white/10'
const headingColor = isDark ? 'text-white' : 'text-fd-navy dark:text-white'
const descColor = isDark ? 'text-white/60' : 'text-fd-navy/60 dark:text-white/60'
const sectionLabel = isDark ? 'text-white/40' : 'text-fd-navy/35 dark:text-white/40'
const summaryLabel = isDark ? 'text-white' : 'text-fd-navy dark:text-white'
const categoryLabel = isDark ? 'text-white/40' : 'text-fd-navy/40 dark:text-white/40'
const dividerClass = isDark ? 'border-white/10' : 'border-fd-navy/8 dark:border-white/10'
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 dark:text-white/70 dark:border-white/20 dark:hover:border-white/50'
const discStr = discount > 0 ? ` (${discountPercent}% rabatt)` : ''
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 id={anchorId || undefined} className={`fd-section ${bgClass}`}>
<div className="fd-container">
{(heading || description) && (
<div className="text-center mb-10 md:mb-14">
{heading && <h2 className={`font-joey-heavy text-fd-h1 mb-3 ${headingColor}`}>{heading}</h2>}
{description && <p className={`font-joey text-fd-body-lg max-w-[600px] mx-auto ${descColor}`}>{description}</p>}
{discount > 0 && (
<span className="inline-block mt-3 px-4 py-1 bg-fd-mint text-fd-navy font-joey-bold text-fd-small rounded-full">
{discountPercent}% rabatt alla resurser
</span>
)}
</div>
)}
<div className="grid grid-cols-1 min-[820px]:grid-cols-5 gap-6 min-[820px]:gap-8">
{/* Left: Config */}
<div className={`min-[820px]:col-span-3 p-7 md:p-9 ${cardClass}`}>
<p className={`font-joey-medium text-fd-body tracking-widest uppercase mb-3 ${sectionLabel}`}>
Operativsystem
</p>
<div className="flex gap-3 mb-6">
{(['linux', 'windows'] as const).map((opt) => (
<button
key={opt}
type="button"
onClick={() => setOs(opt)}
className={`flex-1 py-3 rounded-full text-fd-body-lg border-2 transition-all duration-200 ${os === opt ? osActiveClass : osInactiveClass}`}
>
{opt === 'linux' ? 'Linux (gratis)' : `Windows (+${pricing.windows}kr)`}
</button>
))}
</div>
<div>
<ResourceRow label="CPU-kärnor" value={cpuCores} onChange={setCpuCores} min={1} max={32} unit="st" priceLabel={`${pricing.cpuPerCore} kr/kärna${discStr}`} isDark={isDark} />
<ResourceRow label="RAM" value={ramGb} onChange={setRamGb} min={1} max={256} unit="GB" priceLabel={`${pricing.ramPerGb} kr/GB${discStr}`} isDark={isDark} />
<ResourceRow label="SSD NVMe" value={ssdGb} onChange={setSsdGb} min={0} max={4000} step={10} unit="GB" priceLabel={`${pricing.ssdPerGb} kr/GB${discStr}`} isDark={isDark} />
<ResourceRow label="HDD" value={hddGb} onChange={setHddGb} min={0} max={10000} step={100} unit="GB" priceLabel={`${pricing.hddPerGb} kr/GB${discStr}`} isDark={isDark} />
</div>
{showAdminFee && (
<div className={`flex items-center justify-between py-4 border-b ${dividerClass}`}>
<span className={`font-joey-bold text-fd-body-lg ${summaryLabel}`}>Adminavgift</span>
<span className={`font-joey-medium text-fd-body-lg ${categoryLabel}`}>{formatKr(feeAmount)}</span>
</div>
)}
{hasTillval && (
<div className="mt-7">
<p className={`font-joey-medium text-fd-body tracking-widest uppercase mb-1 ${sectionLabel}`}>Tillvalstjänster</p>
{(additionalServices ?? []).map((svc, i) => (
<div key={i} className={`flex items-center gap-4 py-4 border-b ${dividerClass}`}>
<div className="flex-1 min-w-0">
<span className={`font-joey-bold text-fd-body-lg ${summaryLabel}`}>{svc.label}</span>
{svc.price != null && (
<span className={`block font-joey text-fd-body-lg ${categoryLabel}`}>{svc.price} kr/mån</span>
)}
</div>
<div className="flex-shrink-0 w-14">
<Toggle active={!!extraToggles[i]} onToggle={() => toggleExtra(i)} />
</div>
</div>
))}
</div>
)}
</div>
{/* Right: Summary */}
<div className={`min-[820px]:col-span-2 p-7 md:p-9 flex flex-col ${cardClass}`}>
<h3 className={`font-joey-heavy text-fd-h2 mb-6 ${headingColor}`}>Kostnadsöversikt</h3>
<div className="flex-1 space-y-0">
{baseRows.map((row, i) => (
<div key={i} className={`flex justify-between items-center py-2.5 border-b ${dividerClass}`}>
<span className={`font-joey text-fd-body-lg ${summaryLabel}`}>{row.label}</span>
<span className={`font-joey-bold text-fd-body-lg ${summaryLabel}`}>{formatKr(row.cost)}</span>
</div>
))}
{tillvalRows.length > 0 && (
<>
<div className="pt-4 pb-1">
<span className={`font-joey text-fd-body-lg ${categoryLabel}`}>Tillval</span>
</div>
{tillvalRows.map((row, i) => (
<div key={i} className={`flex justify-between items-center py-2.5 border-b ${dividerClass}`}>
<span className={`font-joey text-fd-body-lg ${summaryLabel}`}>{row.label}</span>
<span className={`font-joey-bold text-fd-body-lg ${summaryLabel}`}>{formatKr(row.cost)}</span>
</div>
))}
</>
)}
</div>
<div className={`mt-6 pt-5 border-t-2 ${dividerClass}`}>
<div className="flex justify-between items-baseline">
<span className={`font-joey text-fd-body-lg ${summaryLabel}`}>Totalt per månad</span>
<span className={`font-joey-heavy text-fd-h1 ${headingColor}`}>{formatKr(costs.total)}</span>
</div>
<p className={`text-right font-joey text-fd-body-lg mt-1 ${categoryLabel}`}>exkl. moms</p>
</div>
<div className="mt-7 space-y-3">
<a href={orderCtaLink || '#'} className="fd-btn-primary w-full justify-center">{orderCtaText}</a>
<a href={contactCtaLink || '#'} className={`${isDark ? 'fd-btn-secondary-dark' : 'fd-btn-secondary dark:fd-btn-secondary-dark'} w-full justify-center`}>
{contactCtaText}
</a>
</div>
</div>
</div>
</div>
</section>
)
}