diff --git a/website/database/seeders/ConfigOptionSeeder.php b/website/database/seeders/ConfigOptionSeeder.php index 3621328..880f402 100644 --- a/website/database/seeders/ConfigOptionSeeder.php +++ b/website/database/seeders/ConfigOptionSeeder.php @@ -498,6 +498,88 @@ class ConfigOptionSeeder extends Seeder ]); $gen14Ipv4->plans()->syncWithoutDetaching($gen14AllPlans); + + // ─── Dedicated 14th Gen — LFF Drive Bays ──────────────────────── + $gen14Lff = PlanConfigGroup::updateOrCreate( + ['name' => 'Dedicated 14th Gen — LFF Drive Bays'], + [ + 'description' => 'Pick a starter drive configuration for the 3.5" LFF bays. Customers needing a heterogeneous setup (mixed sizes per bay, hot spares, etc.) can request a custom layout via the post-order ticket flow.', + 'mode' => 'preset', + 'service_type' => 'dedicated', + 'is_active' => true, + 'sort_order' => 50, + ], + ); + + $gen14LffOption = $this->seedRadioOption($gen14Lff, 'LFF Drive Bays', false, 1); + $this->seedValues($gen14LffOption, [ + ['label' => 'No drives — configure via ticket', 'value' => 'none', 'monthly' => 0, 'is_default' => true], + ['label' => '2× 4 TB SATA HDD', 'value' => '2x4tb-hdd', 'monthly' => 24.00], + ['label' => '4× 4 TB SATA HDD', 'value' => '4x4tb-hdd', 'monthly' => 48.00], + ['label' => '2× 8 TB SATA HDD', 'value' => '2x8tb-hdd', 'monthly' => 40.00], + ['label' => '4× 8 TB SATA HDD', 'value' => '4x8tb-hdd', 'monthly' => 80.00], + ['label' => '8× 8 TB SATA HDD (R540 / R740xd LFF only)', 'value' => '8x8tb-hdd', 'monthly' => 160.00], + ['label' => '12× 8 TB SATA HDD (R740xd LFF only)', 'value' => '12x8tb-hdd', 'monthly' => 240.00], + ]); + + $gen14LffPlanSlugs = ['r440-4lff', 'r540-8lff', 'r740xd-12lff']; + $gen14LffPlans = Plan::query()->whereIn('slug', $gen14LffPlanSlugs)->pluck('id'); + $gen14Lff->plans()->syncWithoutDetaching($gen14LffPlans); + + // ─── Dedicated 14th Gen — SFF Drive Bays ──────────────────────── + $gen14Sff = PlanConfigGroup::updateOrCreate( + ['name' => 'Dedicated 14th Gen — SFF Drive Bays'], + [ + 'description' => 'Pick a starter drive configuration for the 2.5" SFF bays. Mixed-size setups via post-order ticket.', + 'mode' => 'preset', + 'service_type' => 'dedicated', + 'is_active' => true, + 'sort_order' => 51, + ], + ); + + $gen14SffOption = $this->seedRadioOption($gen14Sff, 'SFF Drive Bays', false, 1); + $this->seedValues($gen14SffOption, [ + ['label' => 'No drives — configure via ticket', 'value' => 'none', 'monthly' => 0, 'is_default' => true], + ['label' => '2× 480 GB SATA SSD', 'value' => '2x480gb-ssd', 'monthly' => 20.00], + ['label' => '4× 480 GB SATA SSD', 'value' => '4x480gb-ssd', 'monthly' => 40.00], + ['label' => '2× 1.92 TB SATA SSD', 'value' => '2x1920gb-ssd', 'monthly' => 36.00], + ['label' => '4× 1.92 TB SATA SSD', 'value' => '4x1920gb-ssd', 'monthly' => 72.00], + ['label' => '8× 1.92 TB SATA SSD', 'value' => '8x1920gb-ssd', 'monthly' => 144.00], + ['label' => '16× 1.92 TB SATA SSD (R740 16-bay / R740xd 24-SFF only)', 'value' => '16x1920gb-ssd', 'monthly' => 288.00], + ['label' => '24× 1.92 TB SATA SSD (R740xd 24-SFF only)', 'value' => '24x1920gb-ssd', 'monthly' => 432.00], + ]); + + $gen14SffPlanSlugs = ['r640-8sff', 'r740-16sff', 'r740xd-24sff']; + $gen14SffPlans = Plan::query()->whereIn('slug', $gen14SffPlanSlugs)->pluck('id'); + $gen14Sff->plans()->syncWithoutDetaching($gen14SffPlans); + + // ─── Dedicated 14th Gen — NVMe Drive Bays ─────────────────────── + $gen14Nvme = PlanConfigGroup::updateOrCreate( + ['name' => 'Dedicated 14th Gen — NVMe Drive Bays'], + [ + 'description' => 'Pick a starter NVMe drive configuration. U.2 bays direct-attach to CPU PCIe lanes — no RAID controller in the data path; software RAID (ZFS / mdraid / btrfs) only.', + 'mode' => 'preset', + 'service_type' => 'dedicated', + 'is_active' => true, + 'sort_order' => 52, + ], + ); + + $gen14NvmeOption = $this->seedRadioOption($gen14Nvme, 'NVMe Drive Bays', false, 1); + $this->seedValues($gen14NvmeOption, [ + ['label' => 'No drives — configure via ticket', 'value' => 'none', 'monthly' => 0, 'is_default' => true], + ['label' => '2× 1 TB U.2 NVMe', 'value' => '2x1tb-nvme', 'monthly' => 44.00], + ['label' => '4× 1 TB U.2 NVMe', 'value' => '4x1tb-nvme', 'monthly' => 88.00], + ['label' => '2× 2 TB U.2 NVMe', 'value' => '2x2tb-nvme', 'monthly' => 96.00], + ['label' => '4× 2 TB U.2 NVMe', 'value' => '4x2tb-nvme', 'monthly' => 192.00], + ['label' => '8× 2 TB U.2 NVMe (R640 NVMe 10-bay / R740xd NVMe 24-bay)', 'value' => '8x2tb-nvme', 'monthly' => 384.00], + ['label' => '16× 2 TB U.2 NVMe (R740xd NVMe 24-bay only)', 'value' => '16x2tb-nvme', 'monthly' => 768.00], + ]); + + $gen14NvmePlanSlugs = ['r640-10nvme', 'r740xd-24nvme']; + $gen14NvmePlans = Plan::query()->whereIn('slug', $gen14NvmePlanSlugs)->pluck('id'); + $gen14Nvme->plans()->syncWithoutDetaching($gen14NvmePlans); } /** diff --git a/website/resources/ts/Components/Marketing/Dedicated/DedicatedConfigurator/BuildSummary.vue b/website/resources/ts/Components/Marketing/Dedicated/DedicatedConfigurator/BuildSummary.vue index 41d82ac..3e62c0d 100644 --- a/website/resources/ts/Components/Marketing/Dedicated/DedicatedConfigurator/BuildSummary.vue +++ b/website/resources/ts/Components/Marketing/Dedicated/DedicatedConfigurator/BuildSummary.vue @@ -175,7 +175,7 @@ function formatPrice(amount: number): string {
Setup fee
- Hardware acquisition · charged on first invoice + Server provisioning & deployment · charged on first invoice
@@ -188,7 +188,7 @@ function formatPrice(amount: number): string {
- Non-refundable once hardware is purchased. Switch to Semi-Annual or Annual to waive. + Non-refundable once your build starts. Switch to Semi-Annual or Annual to waive.
@@ -243,16 +243,9 @@ function formatPrice(amount: number): string { box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); - position: sticky; - top: 24px; - max-height: calc(100vh - 48px); - overflow-y: auto; - - @media (max-width: 960px) { - position: relative; - top: auto; - max-height: none; - } + // Stickiness is owned by the parent grid cell (.detail-grid__summary) on + // the page — keeping it here would create a doubly-sticky effect that + // doesn't behave correctly. The card is just a plain card. } .build-summary__head { diff --git a/website/resources/ts/Pages/Marketing/DedicatedServerDetail.vue b/website/resources/ts/Pages/Marketing/DedicatedServerDetail.vue index 49816e1..de4a27c 100644 --- a/website/resources/ts/Pages/Marketing/DedicatedServerDetail.vue +++ b/website/resources/ts/Pages/Marketing/DedicatedServerDetail.vue @@ -145,18 +145,15 @@ const store = useDedicatedConfiguratorStore() - +
-

All main bays ship empty

-

- Drive selection isn't in the v1 self-serve configurator. Once you order, we'll reach out to confirm your drive layout — SATA / SAS / NVMe options, RAID/ZFS preference, and per-bay placement. Drives arrive separately and slot into the pre-installed trays during assembly. -

+

Need a custom drive layout?

- Need specific drives sourced before you order? Open a ticket with your spec — we'll quote the drive cost and add it to your first invoice. + The drive bay configurator covers the most common configurations. If you need a heterogeneous setup (mixed sizes per bay, hot spares, RAID layout preference, specific drive vendor) — open a ticket with your spec and we'll build to it.

@@ -216,7 +213,10 @@ const store = useDedicatedConfiguratorStore() display: grid; grid-template-columns: minmax(0, 1fr) 380px; gap: 32px; - align-items: start; + // Default `stretch` behaviour: the right column grows to the row's full + // height (matching the configurator column). Combined with sticky on + // .detail-grid__summary that gives us a sidebar that stays in view all + // the way down through the configurator scroll. @media (max-width: 1024px) { grid-template-columns: 1fr; @@ -225,10 +225,19 @@ const store = useDedicatedConfiguratorStore() } .detail-grid__summary { - position: relative; + position: sticky; + // 64px navbar (Vuetify VAppBar default) + 16px breathing room. + top: 80px; + align-self: start; // stop the cell from stretching the inner card vertically + max-height: calc(100vh - 96px); + overflow-y: auto; @media (max-width: 1024px) { - order: -1; // on mobile, show summary above configurator so total is visible first + position: relative; + top: auto; + max-height: none; + overflow-y: visible; + order: -1; // mobile: summary above configurator so total is visible first } } diff --git a/website/resources/ts/Pages/Marketing/DedicatedServers.vue b/website/resources/ts/Pages/Marketing/DedicatedServers.vue index 0e8c254..e9184e1 100644 --- a/website/resources/ts/Pages/Marketing/DedicatedServers.vue +++ b/website/resources/ts/Pages/Marketing/DedicatedServers.vue @@ -102,7 +102,7 @@ const faqs = [ }, { question: 'How do setup fees work?', - answer: 'Setup fees range from $349 to $799 depending on chassis class (covers our hardware-acquisition cost). They\'re charged on monthly and quarterly cycles, waived on Semi-Annual and Annual commitments. Once we\'ve placed the hardware order with our supplier (typically within 24 hours of your order), the setup fee is non-refundable. Rental fees still fall under the 14-day money-back guarantee.', + answer: 'Setup fees range from $349 to $799 depending on chassis class. They cover the work of building, racking, configuring iDRAC, and deploying your server. They\'re charged on monthly and quarterly cycles, waived on Semi-Annual and Annual commitments. Once your build starts (typically within 24 hours of your order), the setup fee is non-refundable. Rental fees still fall under the 14-day money-back guarantee.', }, { question: 'What operating systems are supported?', diff --git a/website/tests/Feature/Marketing/DedicatedServersTest.php b/website/tests/Feature/Marketing/DedicatedServersTest.php index 258fa0a..db2e6ed 100644 --- a/website/tests/Feature/Marketing/DedicatedServersTest.php +++ b/website/tests/Feature/Marketing/DedicatedServersTest.php @@ -115,19 +115,40 @@ test('non-R740xd chassis get the standard CPU group', function (): void { expect($names)->not->toContain('Dedicated 14th Gen — CPU Upgrade (R740xd)'); }); -test('seeder creates the 6 dedicated 14th-gen config groups', function (): void { +test('seeder creates the 9 dedicated 14th-gen config groups', function (): void { $names = PlanConfigGroup::query() ->where('name', 'like', 'Dedicated 14th Gen%') ->pluck('name') ->all(); - expect(count($names))->toBe(6); + expect(count($names))->toBe(9); expect($names)->toContain('Dedicated 14th Gen — CPU Upgrade'); expect($names)->toContain('Dedicated 14th Gen — CPU Upgrade (R740xd)'); expect($names)->toContain('Dedicated 14th Gen — RAM Upgrade'); expect($names)->toContain('Dedicated 14th Gen — Operating System'); expect($names)->toContain('Dedicated 14th Gen — Bandwidth'); expect($names)->toContain('Dedicated 14th Gen — IPv4 Block'); + expect($names)->toContain('Dedicated 14th Gen — LFF Drive Bays'); + expect($names)->toContain('Dedicated 14th Gen — SFF Drive Bays'); + expect($names)->toContain('Dedicated 14th Gen — NVMe Drive Bays'); +}); + +test('drive bay groups are attached to chassis by bay type', function (): void { + // LFF chassis should have LFF Drive Bays attached + $r440 = Plan::where('slug', 'r440-4lff')->first(); + expect($r440->configGroups->pluck('name'))->toContain('Dedicated 14th Gen — LFF Drive Bays'); + expect($r440->configGroups->pluck('name'))->not->toContain('Dedicated 14th Gen — SFF Drive Bays'); + expect($r440->configGroups->pluck('name'))->not->toContain('Dedicated 14th Gen — NVMe Drive Bays'); + + // SFF chassis should have SFF Drive Bays + $r640 = Plan::where('slug', 'r640-8sff')->first(); + expect($r640->configGroups->pluck('name'))->toContain('Dedicated 14th Gen — SFF Drive Bays'); + expect($r640->configGroups->pluck('name'))->not->toContain('Dedicated 14th Gen — LFF Drive Bays'); + + // NVMe chassis should have NVMe Drive Bays + $nvme = Plan::where('slug', 'r740xd-24nvme')->first(); + expect($nvme->configGroups->pluck('name'))->toContain('Dedicated 14th Gen — NVMe Drive Bays'); + expect($nvme->configGroups->pluck('name'))->not->toContain('Dedicated 14th Gen — SFF Drive Bays'); }); test('RAM upgrade group standardizes on DDR4-2400 across all tiers', function (): void {