diff --git a/website/resources/ts/Components/Marketing/Dedicated/BuildStatusPanel.vue b/website/resources/ts/Components/Marketing/Dedicated/BuildStatusPanel.vue new file mode 100644 index 0000000..f504a42 --- /dev/null +++ b/website/resources/ts/Components/Marketing/Dedicated/BuildStatusPanel.vue @@ -0,0 +1,238 @@ + + + + + diff --git a/website/resources/ts/Components/Marketing/Dedicated/ChassisCard.vue b/website/resources/ts/Components/Marketing/Dedicated/ChassisCard.vue new file mode 100644 index 0000000..72b85fa --- /dev/null +++ b/website/resources/ts/Components/Marketing/Dedicated/ChassisCard.vue @@ -0,0 +1,255 @@ + + + + + + + diff --git a/website/resources/ts/Components/Marketing/Dedicated/DedicatedConfigurator/ConfiguratorFooter.vue b/website/resources/ts/Components/Marketing/Dedicated/DedicatedConfigurator/ConfiguratorFooter.vue new file mode 100644 index 0000000..d2a9bdc --- /dev/null +++ b/website/resources/ts/Components/Marketing/Dedicated/DedicatedConfigurator/ConfiguratorFooter.vue @@ -0,0 +1,119 @@ + + + + + diff --git a/website/resources/ts/Components/Marketing/Dedicated/DedicatedConfigurator/CycleToggle.vue b/website/resources/ts/Components/Marketing/Dedicated/DedicatedConfigurator/CycleToggle.vue new file mode 100644 index 0000000..38d1103 --- /dev/null +++ b/website/resources/ts/Components/Marketing/Dedicated/DedicatedConfigurator/CycleToggle.vue @@ -0,0 +1,91 @@ + + + + + diff --git a/website/resources/ts/Components/Marketing/Dedicated/DedicatedConfigurator/OptionGroupSelector.vue b/website/resources/ts/Components/Marketing/Dedicated/DedicatedConfigurator/OptionGroupSelector.vue new file mode 100644 index 0000000..c222c6c --- /dev/null +++ b/website/resources/ts/Components/Marketing/Dedicated/DedicatedConfigurator/OptionGroupSelector.vue @@ -0,0 +1,120 @@ + + + + + diff --git a/website/resources/ts/Components/Marketing/Dedicated/DedicatedConfigurator/index.vue b/website/resources/ts/Components/Marketing/Dedicated/DedicatedConfigurator/index.vue new file mode 100644 index 0000000..9a63140 --- /dev/null +++ b/website/resources/ts/Components/Marketing/Dedicated/DedicatedConfigurator/index.vue @@ -0,0 +1,125 @@ + + + + + diff --git a/website/resources/ts/Components/Marketing/Dedicated/GenerationFilter.vue b/website/resources/ts/Components/Marketing/Dedicated/GenerationFilter.vue new file mode 100644 index 0000000..b8b9e5b --- /dev/null +++ b/website/resources/ts/Components/Marketing/Dedicated/GenerationFilter.vue @@ -0,0 +1,88 @@ + + + + + diff --git a/website/resources/ts/Pages/Marketing/DedicatedServerDetail.vue b/website/resources/ts/Pages/Marketing/DedicatedServerDetail.vue new file mode 100644 index 0000000..0b9536d --- /dev/null +++ b/website/resources/ts/Pages/Marketing/DedicatedServerDetail.vue @@ -0,0 +1,199 @@ + + + + + diff --git a/website/resources/ts/Pages/Marketing/DedicatedServers.vue b/website/resources/ts/Pages/Marketing/DedicatedServers.vue index 45d5720..7b83969 100644 --- a/website/resources/ts/Pages/Marketing/DedicatedServers.vue +++ b/website/resources/ts/Pages/Marketing/DedicatedServers.vue @@ -1,12 +1,14 @@ - + - - - -
- - - -
-

{{ feature.title }}

-

{{ feature.description }}

-
-
-
-
+
+
Built on
+
    +
  • + + Dell PowerEdge +
  • +
  • + + Intel Xeon Gold (Cascade Lake) +
  • +
  • + + DDR4-2400 ECC +
  • +
  • + + BOSS M.2 boot · all bays free for data +
  • +
  • + + iDRAC9 Enterprise +
  • +
  • + + Atlanta, GA datacenter +
  • +
+
- -
+ +
- - - + +
+ +
+

+ Build to order — Dell 14th gen + + 7-10 business days · setup fee waived on 6+ month commitments + +

+ + - - -
-

{{ plan.name }}

- - {{ isInStock(plan) ? 'In Stock' : 'Sold Out' }} - -
+ +
+
+
-

{{ getFeature(plan, 'storage_bays') }}

- - -
- {{ formatPrice(plan) }} - /mo -
- - - - -
-
- - {{ getFeature(plan, 'cpu') }} -
-
- - {{ getFeature(plan, 'cores') }} -
-
- - {{ getFeature(plan, 'ram') }} RAM -
-
- - {{ getFeature(plan, 'storage_bays') }} -
-
- - - - - Order Now - - - - Unavailable - - - - - +
+

+ In stock — rack inventory (12th/13th gen) + + Ready to ship in 1-2 business days · no setup fee + +

+ + + + + +
- + - - -
- - - -

{{ item.label }}

-
-
-
+
+ +
+ + + + +
+ -
+
-

Need a Custom Configuration?

+

Need something custom?

- Contact us for custom builds, bulk orders, or servers with specific hardware requirements. + If your workload doesn't fit the catalog — colocation, custom GPU, multi-server clusters — talk to us. We build to spec.

@@ -244,7 +293,50 @@ function formatPrice(plan: Plan): string { diff --git a/website/resources/ts/stores/dedicatedConfigurator.ts b/website/resources/ts/stores/dedicatedConfigurator.ts new file mode 100644 index 0000000..fa38b40 --- /dev/null +++ b/website/resources/ts/stores/dedicatedConfigurator.ts @@ -0,0 +1,252 @@ +import { defineStore } from 'pinia' +import { computed, ref } from 'vue' + +export interface DedicatedPlan { + id: number + slug: string + name: string + price: string + setup_fee?: string | number + features: Record | null + prices?: Array<{ billing_cycle: string; price: string }> +} + +export interface DedicatedConfigValue { + id: number + label: string + value: string + monthly_price: string + quarterly_price: string + semi_annual_price: string + annual_price: string + is_default: boolean +} + +export interface DedicatedConfigOption { + id: number + name: string + type: string + values: DedicatedConfigValue[] +} + +export interface DedicatedConfigGroup { + id: number + name: string + description: string | null + options: DedicatedConfigOption[] + sort_order: number +} + +export type DedicatedCycle = 'monthly' | 'quarterly' | 'semi_annual' | 'annual' + +export const CYCLE_MONTHS: Record = { + monthly: 1, + quarterly: 3, + semi_annual: 6, + annual: 12, +} + +const CYCLES_WITH_SETUP_FEE: DedicatedCycle[] = ['monthly', 'quarterly'] + +export const useDedicatedConfiguratorStore = defineStore('dedicatedConfigurator', () => { + const plan = ref(null) + const configGroups = ref([]) + const accountUrl = ref('') + + // selections: groupName → option value slug (e.g., {"Dedicated 14th Gen — RAM Upgrade": "64"}) + const selections = ref>({}) + const cycle = ref('monthly') + + function init(catalog: { + plan: DedicatedPlan + configGroups: DedicatedConfigGroup[] + accountUrl: string + }): void { + plan.value = catalog.plan + configGroups.value = catalog.configGroups + accountUrl.value = catalog.accountUrl + + // Seed selections with each group's default value (or first value if no default). + const seeded: Record = {} + for (const group of catalog.configGroups) { + const opt = group.options[0] + if (!opt) continue + const def = opt.values.find(v => v.is_default) ?? opt.values[0] + if (def) seeded[group.name] = def.value + } + selections.value = seeded + } + + function setSelection(groupName: string, value: string): void { + selections.value = { ...selections.value, [groupName]: value } + } + + function findGroup(groupName: string): DedicatedConfigGroup | null { + return configGroups.value.find(g => g.name === groupName) ?? null + } + + function findValue(groupName: string, valueSlug: string): DedicatedConfigValue | null { + const g = findGroup(groupName) + if (!g || !g.options[0]) return null + return g.options[0].values.find(v => v.value === valueSlug) ?? null + } + + function pickCyclePrice(value: DedicatedConfigValue, c: DedicatedCycle): number { + const raw = c === 'monthly' + ? value.monthly_price + : c === 'quarterly' + ? value.quarterly_price + : c === 'semi_annual' + ? value.semi_annual_price + : value.annual_price + return raw ? parseFloat(raw) : 0 + } + + function planPriceForCycle(c: DedicatedCycle): number { + if (!plan.value) return 0 + if (plan.value.prices && plan.value.prices.length > 0) { + const pp = plan.value.prices.find(p => p.billing_cycle === c) + if (pp) return parseFloat(pp.price) + } + return parseFloat(plan.value.price) * CYCLE_MONTHS[c] + } + + const baselinePrice = computed(() => planPriceForCycle(cycle.value)) + + const addOnsTotal = computed(() => { + let total = 0 + for (const [groupName, valueSlug] of Object.entries(selections.value)) { + const v = findValue(groupName, valueSlug) + if (v) total += pickCyclePrice(v, cycle.value) + } + return total + }) + + const setupFee = computed(() => { + if (!plan.value) return 0 + const fee = parseFloat(String(plan.value.setup_fee ?? 0)) + if (fee <= 0) return 0 + return CYCLES_WITH_SETUP_FEE.includes(cycle.value) ? fee : 0 + }) + + const cycleSubtotal = computed(() => baselinePrice.value + addOnsTotal.value) + + const cycleTotal = computed(() => cycleSubtotal.value + setupFee.value) + + const monthlyEffective = computed(() => cycleSubtotal.value / CYCLE_MONTHS[cycle.value]) + + const isSetupFeeWaived = computed(() => { + if (!plan.value) return true + const fee = parseFloat(String(plan.value.setup_fee ?? 0)) + if (fee <= 0) return true + return !CYCLES_WITH_SETUP_FEE.includes(cycle.value) + }) + + // Build the share URL with all current selections + cycle as query params. + // Param keys are short and readable so URLs stay shareable. + const shareUrl = computed(() => { + if (!plan.value) return '' + const params = new URLSearchParams() + if (cycle.value !== 'monthly') params.set('cycle', cycle.value) + for (const [groupName, valueSlug] of Object.entries(selections.value)) { + const param = groupNameToParam(groupName) + if (!param) continue + const g = findGroup(groupName) + const def = g?.options[0]?.values.find(v => v.is_default)?.value + // Only add to URL if non-default + if (def && valueSlug === def) continue + params.set(param, valueSlug) + } + const qs = params.toString() + if (typeof window === 'undefined') return qs ? `/dedicated-servers/${plan.value.slug}?${qs}` : `/dedicated-servers/${plan.value.slug}` + return qs ? `${window.location.origin}${window.location.pathname}?${qs}` : `${window.location.origin}${window.location.pathname}` + }) + + const checkoutUrl = computed(() => { + if (!plan.value) return '' + const params = new URLSearchParams() + if (cycle.value !== 'monthly') params.set('cycle', cycle.value) + for (const [groupName, valueSlug] of Object.entries(selections.value)) { + const param = groupNameToParam(groupName) + if (!param) continue + const g = findGroup(groupName) + const def = g?.options[0]?.values.find(v => v.is_default)?.value + if (def && valueSlug === def) continue + params.set(param, valueSlug) + } + const qs = params.toString() + const base = `${accountUrl.value}/checkout/${plan.value.id}` + return qs ? `${base}?${qs}` : base + }) + + function groupNameToParam(groupName: string): string | null { + const map: Record = { + 'Dedicated 14th Gen — CPU Upgrade': 'cpu', + 'Dedicated 14th Gen — CPU Upgrade (R740xd)': 'cpu', + 'Dedicated 14th Gen — RAM Upgrade': 'ram', + 'Dedicated 14th Gen — Operating System': 'os', + 'Dedicated 14th Gen — Bandwidth': 'bw', + 'Dedicated 14th Gen — IPv4 Block': 'ipv4', + } + return map[groupName] ?? null + } + + function paramToGroupName(param: string): string[] { + // Returns matching group names for a query param. Some params (cpu) map to + // multiple groups depending on chassis (CPU vs CPU R740xd) — we set on + // every matching group, the visible one (only one is attached) wins. + const map: Record = { + cpu: ['Dedicated 14th Gen — CPU Upgrade', 'Dedicated 14th Gen — CPU Upgrade (R740xd)'], + ram: ['Dedicated 14th Gen — RAM Upgrade'], + os: ['Dedicated 14th Gen — Operating System'], + bw: ['Dedicated 14th Gen — Bandwidth'], + ipv4: ['Dedicated 14th Gen — IPv4 Block'], + } + return map[param] ?? [] + } + + function hydrateFromUrl(search: string): void { + const p = new URLSearchParams(search) + + const c = p.get('cycle') + if (c && (['monthly', 'quarterly', 'semi_annual', 'annual'] as const).includes(c as DedicatedCycle)) { + cycle.value = c as DedicatedCycle + } + + for (const param of ['cpu', 'ram', 'os', 'bw', 'ipv4']) { + const v = p.get(param) + if (!v) continue + for (const groupName of paramToGroupName(param)) { + const group = findGroup(groupName) + if (!group) continue + const validValue = group.options[0]?.values.find(val => val.value === v) + if (validValue) { + selections.value[groupName] = v + } + } + } + } + + return { + plan, + configGroups, + accountUrl, + selections, + cycle, + baselinePrice, + addOnsTotal, + setupFee, + cycleSubtotal, + cycleTotal, + monthlyEffective, + isSetupFeeWaived, + shareUrl, + checkoutUrl, + init, + setSelection, + hydrateFromUrl, + findGroup, + findValue, + pickCyclePrice, + } +})