This commit is contained in:
Rostislav Dugin
2026-03-23 17:38:41 +03:00
parent b541e519dc
commit d6e6ef0c5c

View File

@@ -101,204 +101,211 @@ export function PriceCalculatorComponent() {
<div className="absolute top-0 right-0 w-[250px] h-[250px] bg-[#155dfc]/5 rounded-full blur-3xl pointer-events-none" />
<div className="relative">
{/* Storage size slider */}
<div className="mb-5">
<div className="flex items-baseline mb-2">
<label className=" md:text-base font-medium min-w-[125px]">
Storage size
</label>
<div className="md:grid md:grid-cols-2 md:gap-6">
{/* Left column: sliders */}
<div>
{/* Storage size slider */}
<div className="mb-5">
<div className="flex items-baseline mb-2">
<label className=" md:text-base font-medium min-w-[125px]">
Storage size
</label>
<span className=" md:text-base font-bold text-blue-500">
{formatSize(storageSizeGb)}
</span>
</div>
<input
type="range"
className={styles.calcSlider}
style={sliderBackground(
effectiveStoragePos,
STORAGE_SIZE_STEPS.length - 1,
)}
min={0}
max={STORAGE_SIZE_STEPS.length - 1}
value={effectiveStoragePos}
onChange={(e) => setStorageSliderPos(Number(e.target.value))}
/>
<div className="flex justify-between mt-1.5 text-gray-500">
<span>{formatSize(MIN_GB_AMOUNT)}</span>
<span>10 TB</span>
</div>
</div>
{/* Single backup size slider */}
<div className="mb-4">
<div className="flex items-baseline mb-1">
<label className=" md:text-base font-medium min-w-[185px]">
Single backup size
</label>
<span className=" md:text-base font-bold text-blue-500">
{formatSize(singleBackupSizeGb)}
</span>
</div>
<p className="mb-2 text-gray-400 flex items-center">
<span className="flex items-center gap-1 min-w-[175px]">
Approximate DB size{" "}
<span className="relative inline-block group">
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="inline -mt-0.5 text-gray-500 cursor-help"
>
<circle cx="12" cy="12" r="10" />
<path d="M12 16v-4M12 8h.01" />
</svg>
<span className="pointer-events-none absolute bottom-full left-1/2 -translate-x-1/2 mb-2 w-52 rounded-lg bg-[#1f2937] border border-[#ffffff20] px-3 py-2 text-gray-300 opacity-0 group-hover:opacity-100 transition-opacity">
Estimated with ~10x compression ratio typical for database
backups. Can differ based on the database type, structure,
and content.
<span className=" md:text-base font-bold text-blue-500">
{formatSize(storageSizeGb)}
</span>
</span>
</span>
</div>
<span className="text-gray-200 font-medium">
~{formatSize(approximateDbSize)}
</span>
</p>
<input
type="range"
className={styles.calcSlider}
style={sliderBackground(
effectiveStoragePos,
STORAGE_SIZE_STEPS.length - 1,
)}
min={0}
max={STORAGE_SIZE_STEPS.length - 1}
value={effectiveStoragePos}
onChange={(e) => setStorageSliderPos(Number(e.target.value))}
/>
{/* DB size commands */}
<details className="mb-2 group">
<summary className=" text-gray-500 cursor-pointer hover:text-gray-400 transition-colors list-none flex items-center gap-1.5">
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="transition-transform group-open:rotate-90"
>
<path d="M9 18l6-6-6-6" />
</svg>
How to check DB size?
</summary>
<div className="mt-2 space-y-1.5">
{DB_SIZE_COMMANDS.map((cmd, index) => (
<div key={index}>
<p className=" text-gray-400 mb-1">{cmd.label}</p>
<div className="relative">
<pre className="rounded-lg bg-[#1f2937] border border-[#ffffff20] px-2.5 py-1.5 pr-16 overflow-x-auto">
<code className="block whitespace-pre text-gray-300">
{cmd.code}
</code>
</pre>
<button
onClick={async () => {
try {
await navigator.clipboard.writeText(cmd.code);
setCopiedIndex(index);
setTimeout(() => setCopiedIndex(null), 2000);
} catch {}
}}
className={`absolute right-2 top-2 rounded px-2 py-0.5 text-white transition-colors border border-[#ffffff20] ${
copiedIndex === index
? "bg-green-500"
: "bg-blue-600 hover:bg-blue-700"
}`}
>
{copiedIndex === index ? "Copied!" : "Copy"}
</button>
</div>
</div>
))}
<div className="flex justify-between mt-1.5 text-gray-500">
<span>{formatSize(MIN_GB_AMOUNT)}</span>
<span>10 TB</span>
</div>
</div>
</details>
<input
type="range"
className={styles.calcSlider}
style={sliderBackground(
backupSliderPos,
BACKUP_SIZE_STEPS.length - 1,
)}
min={0}
max={BACKUP_SIZE_STEPS.length - 1}
value={backupSliderPos}
onChange={(e) => setBackupSliderPos(Number(e.target.value))}
/>
{/* Single backup size slider */}
<div className="mb-4">
<div className="flex items-baseline mb-1">
<label className=" md:text-base font-medium min-w-[185px]">
Single backup size
</label>
<div className="flex justify-between mt-1.5 text-gray-500">
<span>1 GB</span>
<span>200 GB</span>
</div>
</div>
<span className=" md:text-base font-bold text-blue-500">
{formatSize(singleBackupSizeGb)}
</span>
</div>
{/* Retention estimation (GFS) */}
<div className="mb-5">
<p className=" md:text-base font-medium mb-2">
Estimated retention (GFS)
</p>
<p className=" text-gray-400 mb-4">
Keeps recent backups frequently, older ones less often broad
time at the lowest cost
</p>
{(() => {
const gfs = distributeGfs(backupsFit);
return (
<div className="space-y-2">
<div className="bg-[#1f2937]/50 border border-[#ffffff20] rounded-lg px-4 py-3 text-center">
<p className=" text-gray-500 mb-0.5">Total backups</p>
<p className="text-lg md:text-xl font-bold text-gray-200">
{backupsFit}
</p>
</div>
<p className=" text-gray-400 mb-4 mt-3">
It is enough to keep the following amount of backups:
</p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
{(
[
["Daily", gfs.daily],
["Weekly", gfs.weekly],
["Monthly", gfs.monthly],
["Yearly", gfs.yearly],
] as const
).map(([label, value]) => (
<div
key={label}
className="bg-[#1f2937]/50 border border-[#ffffff20] rounded-lg px-3 py-2.5 text-center"
<p className="mb-2 text-gray-400 flex items-center">
<span className="flex items-center gap-1 min-w-[175px]">
Approximate DB size{" "}
<span className="relative inline-block group">
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="inline -mt-0.5 text-gray-500 cursor-help"
>
<p className=" text-gray-500 mb-0.5">{label}</p>
<p className="text-lg font-bold text-gray-200">
{value}
</p>
<circle cx="12" cy="12" r="10" />
<path d="M12 16v-4M12 8h.01" />
</svg>
<span className="pointer-events-none absolute bottom-full left-1/2 -translate-x-1/2 mb-2 w-52 rounded-lg bg-[#1f2937] border border-[#ffffff20] px-3 py-2 text-gray-300 opacity-0 group-hover:opacity-100 transition-opacity">
Estimated with ~10x compression ratio typical for
database backups. Can differ based on the database type,
structure, and content.
</span>
</span>
</span>
<span className="text-gray-200 font-medium">
~{formatSize(approximateDbSize)}
</span>
</p>
{/* DB size commands */}
<details className="mb-2 group">
<summary className=" text-gray-500 cursor-pointer hover:text-gray-400 transition-colors list-none flex items-center gap-1.5">
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="transition-transform group-open:rotate-90"
>
<path d="M9 18l6-6-6-6" />
</svg>
How to check DB size?
</summary>
<div className="mt-2 space-y-1.5">
{DB_SIZE_COMMANDS.map((cmd, index) => (
<div key={index}>
<p className=" text-gray-400 mb-1">{cmd.label}</p>
<div className="relative">
<pre className="rounded-lg bg-[#1f2937] border border-[#ffffff20] px-2.5 py-1.5 pr-16 overflow-x-auto">
<code className="block whitespace-pre text-gray-300">
{cmd.code}
</code>
</pre>
<button
onClick={async () => {
try {
await navigator.clipboard.writeText(cmd.code);
setCopiedIndex(index);
setTimeout(() => setCopiedIndex(null), 2000);
} catch {}
}}
className={`absolute right-2 top-2 rounded px-2 py-0.5 text-white transition-colors border border-[#ffffff20] ${
copiedIndex === index
? "bg-green-500"
: "bg-blue-600 hover:bg-blue-700"
}`}
>
{copiedIndex === index ? "Copied!" : "Copy"}
</button>
</div>
</div>
))}
</div>
</div>
);
})()}
</details>
<p className=" text-gray-400 mt-3">
You can fine-tune retention values (change daily coutn, keep only
monthly, keep N latest, etc.)
</p>
<input
type="range"
className={styles.calcSlider}
style={sliderBackground(
backupSliderPos,
BACKUP_SIZE_STEPS.length - 1,
)}
min={0}
max={BACKUP_SIZE_STEPS.length - 1}
value={backupSliderPos}
onChange={(e) => setBackupSliderPos(Number(e.target.value))}
/>
<div className="flex justify-between mt-1.5 text-gray-500">
<span>1 GB</span>
<span>200 GB</span>
</div>
</div>
</div>
{/* Right column: retention */}
<div>
{/* Retention estimation (GFS) */}
<div>
{(() => {
const gfs = distributeGfs(backupsFit);
return (
<div className="space-y-1.5">
<div className="bg-[#1f2937]/50 border border-[#ffffff20] rounded-lg px-3 py-2 text-center">
<p className=" text-gray-500 mb-0.5">Total backups</p>
<p className="text-lg font-bold text-gray-200">
{backupsFit}
</p>
</div>
<div className="flex items-center gap-3 my-1">
<div className="flex-1 h-px bg-[#ffffff20]" />
<span className="text-sm text-gray-500">or</span>
<div className="flex-1 h-px bg-[#ffffff20]" />
</div>
<p className=" text-gray-400 mb-2 md:text-sm">
Keeps recent backups frequently, older ones less often
broad time at the lowest cost. It is enough to keep the
following amount of backups in GFS:
</p>
<div className="grid grid-cols-2 gap-1.5">
{(
[
["Daily", gfs.daily],
["Weekly", gfs.weekly],
["Monthly", gfs.monthly],
["Yearly", gfs.yearly],
] as const
).map(([label, value]) => (
<div
key={label}
className="bg-[#1f2937]/50 border border-[#ffffff20] rounded-lg px-2 py-1.5 text-center"
>
<p className="text-xs text-gray-500">{label}</p>
<p className="text-base font-bold text-gray-200">
{value}
</p>
</div>
))}
</div>
</div>
);
})()}
<p className=" text-gray-400 mt-2 md:text-sm mb-3">
You can fine-tune retention values (change daily count, keep
only monthly, keep N latest, etc.)
</p>
</div>
</div>
</div>
{/* Price display */}