From a224051bdeb1d8c6d48e5910059fa516829b2d46ff0f05f1c122ae21c9580e29 Mon Sep 17 00:00:00 2001 From: Andrew Date: Sun, 26 Apr 2026 20:48:53 -0400 Subject: [PATCH] feat(dedicated): dropdowns for radio groups, card-grid OS picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces stacked radio cards with VSelect dropdowns across CPU, RAM, Bandwidth, IPv4, Private Networking, PCIe NVMe Add-in, and the Drive Selection control inside each drive bay group. Major space savings — the LFF Drive Selection alone goes from 15 stacked cards to one row on screen, with the active price still visible at a glance via the selection slot's right-aligned chip. OS group becomes a tile-grid picker (`OsGroupSelector.vue`): 6 cards with brand logos, distro name, and price chip. Logos shipped as hand-authored SVGs at public/img/os/{ubuntu,debian,almalinux,rocky, windows,no-os}.svg — no new npm dependency. - Synchronous Pinia store init: moved store.init() out of onMounted into setup so children's `selected` props are populated on first render. Without this VSelect's selection slot fires with a stub item before init completes and the whole tree throws on a defensive `.toFixed` access. - Defensive priceLabel guards in both OptionGroupSelector and DriveBayGroupSelector for any future re-render where the slot's raw item is incomplete. - isOperatingSystemGroup() helper alongside isDriveBayGroup() in the store; configurator switches OS → OsGroupSelector, drive bays → DriveBayGroupSelector, everything else → OptionGroupSelector. Co-Authored-By: Claude Opus 4.7 (1M context) --- website/public/img/os/almalinux.svg | 5 + website/public/img/os/debian.svg | 5 + website/public/img/os/no-os.svg | 10 + website/public/img/os/rocky.svg | 7 + website/public/img/os/ubuntu.svg | 7 + website/public/img/os/windows.svg | 7 + .../DriveBayGroupSelector.vue | 180 ++++++++++++----- .../OptionGroupSelector.vue | 182 ++++++++++++----- .../DedicatedConfigurator/OsGroupSelector.vue | 185 ++++++++++++++++++ .../Dedicated/DedicatedConfigurator/index.vue | 31 ++- .../ts/stores/dedicatedConfigurator.ts | 4 + 11 files changed, 506 insertions(+), 117 deletions(-) create mode 100644 website/public/img/os/almalinux.svg create mode 100644 website/public/img/os/debian.svg create mode 100644 website/public/img/os/no-os.svg create mode 100644 website/public/img/os/rocky.svg create mode 100644 website/public/img/os/ubuntu.svg create mode 100644 website/public/img/os/windows.svg create mode 100644 website/resources/ts/Components/Marketing/Dedicated/DedicatedConfigurator/OsGroupSelector.vue diff --git a/website/public/img/os/almalinux.svg b/website/public/img/os/almalinux.svg new file mode 100644 index 0000000..efd9050 --- /dev/null +++ b/website/public/img/os/almalinux.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/website/public/img/os/debian.svg b/website/public/img/os/debian.svg new file mode 100644 index 0000000..7695fd5 --- /dev/null +++ b/website/public/img/os/debian.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/website/public/img/os/no-os.svg b/website/public/img/os/no-os.svg new file mode 100644 index 0000000..bbe6fc7 --- /dev/null +++ b/website/public/img/os/no-os.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/website/public/img/os/rocky.svg b/website/public/img/os/rocky.svg new file mode 100644 index 0000000..d558786 --- /dev/null +++ b/website/public/img/os/rocky.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/website/public/img/os/ubuntu.svg b/website/public/img/os/ubuntu.svg new file mode 100644 index 0000000..686b28c --- /dev/null +++ b/website/public/img/os/ubuntu.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/website/public/img/os/windows.svg b/website/public/img/os/windows.svg new file mode 100644 index 0000000..1659871 --- /dev/null +++ b/website/public/img/os/windows.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/website/resources/ts/Components/Marketing/Dedicated/DedicatedConfigurator/DriveBayGroupSelector.vue b/website/resources/ts/Components/Marketing/Dedicated/DedicatedConfigurator/DriveBayGroupSelector.vue index f268e64..970e0af 100644 --- a/website/resources/ts/Components/Marketing/Dedicated/DedicatedConfigurator/DriveBayGroupSelector.vue +++ b/website/resources/ts/Components/Marketing/Dedicated/DedicatedConfigurator/DriveBayGroupSelector.vue @@ -46,17 +46,47 @@ function priceFor(v: DedicatedConfigValue): number { return raw ? parseFloat(raw) : 0 } -function priceLabel(v: DedicatedConfigValue): string { - const p = priceFor(v) - if (p === 0) return v.is_default ? 'no drives' : 'free' - const suffix = props.cycle === 'monthly' +const perDriveSuffix = computed(() => + props.cycle === 'monthly' ? '/drive/mo' : props.cycle === 'quarterly' ? '/drive/qtr' : props.cycle === 'semi_annual' ? '/drive/6mo' - : '/drive/yr' - return `+$${p.toFixed(2)}${suffix}` + : '/drive/yr', +) + +function priceLabel(v: DedicatedConfigValue): string { + const p = priceFor(v) + if (p === 0) return v.is_default ? 'no drives' : 'free' + return `+$${p.toFixed(2)}${perDriveSuffix.value}` +} + +interface DriveItem { + value: string + label: string + price: number + isDefault: boolean +} + +const driveItems = computed(() => + driveValues.value.map(v => ({ + value: v.value, + label: v.label, + price: priceFor(v), + isDefault: v.is_default, + })), +) + +function driveItemLabel(item: DriveItem | { price?: number; isDefault?: boolean } | undefined): string { + const price = item?.price + const isDefault = item?.isDefault + if (typeof price !== 'number' || price === 0) return isDefault ? 'no drives' : 'free' + return `+$${price.toFixed(2)}${perDriveSuffix.value}` +} + +function onDriveChange(v: unknown): void { + if (typeof v === 'string') pickDrive(v) } const cycleSuffix = computed(() => @@ -105,32 +135,44 @@ function pickDrive(value: string): void {
Drive selection
-
-