Files
website/website/resources/ts/Pages/Marketing/VpsHosting.vue
Andrew 66a65263c3 fix(vps): drop Windows License toggle from estimator
Windows BYOL is free and doesn't change the price, so the toggle
was UI clutter. The Included With All Plans card already
communicates the BYOL requirement (line updated to read
"Linux included; Windows supported (bring your own license)" for
clarity), and OS template selection at checkout still lets users
pick a Windows image when they're ready to deploy.

- AddOnsPanel.vue: removed the Windows License switch
- EstimatorSection.vue: removed onWindowsChange handler
- stores/estimator.ts: removed windowsLicense state, dropped from
  share/checkout URL params and hydration
- ConfigOptionSeeder.php: dropped the legacy 'Windows License'
  option from the VPS Add-ons group (deletes existing row)
- CheckoutController.php: removed ?windows=1 query-param handling
  from buildPrefilledSelections
- VpsHosting.vue: tightened the BYOL line in the Included card

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 16:28:03 -04:00

279 lines
10 KiB
Vue

<script lang="ts" setup>
import { Head, usePage } from '@inertiajs/vue3'
import { computed } from 'vue'
import MarketingLayout from '@/Layouts/MarketingLayout.vue'
import SectionHeader from '@/Components/Marketing/SectionHeader.vue'
import HeroSection from '@/Components/Marketing/HeroSection.vue'
import VpsHero from '@/Components/Marketing/VpsHero.vue'
import EstimatorSection from '@/Components/Marketing/Estimator/EstimatorSection.vue'
import { useEstimatorStore, type EstimatorPlan, type EstimatorAddOnGroup, type WorkloadEntry, type AppExample } from '@/stores/estimator'
import { crossDomainUrl } from '@/utils/resolvers'
defineOptions({ layout: MarketingLayout })
interface PageProps {
plans: EstimatorPlan[]
addOns: EstimatorAddOnGroup[]
workloadMap: Record<string, WorkloadEntry>
appExamples: AppExample[]
domains: { marketing: string; account: string; admin: string }
}
interface Feature {
icon: string
title: string
description: string
}
interface IncludedItem {
text: string
comingSoon?: boolean
}
const page = usePage()
const props = computed(() => page.props as unknown as PageProps)
const accountUrl = computed<string>(() => crossDomainUrl(props.value.domains?.account))
const plans = computed<EstimatorPlan[]>(() => props.value.plans || [])
const addOns = computed<EstimatorAddOnGroup[]>(() => props.value.addOns || [])
const workloadMap = computed<Record<string, WorkloadEntry>>(() => props.value.workloadMap || {})
const appExamples = computed<AppExample[]>(() => props.value.appExamples || [])
const estimator = useEstimatorStore()
const startingPrice = computed<string>(() => {
if (plans.value.length === 0) return '5.00'
const lowest = Math.min(...plans.value.map(p => parseFloat(p.price)))
return lowest % 1 === 0 ? lowest.toString() : lowest.toFixed(2)
})
const features: Feature[] = [
{ icon: 'tabler-database', title: 'RAID 10 SSD Storage', description: 'Redundant SSD arrays for fast read/write speeds and data protection.' },
{ icon: 'tabler-server', title: 'KVM Virtualization', description: 'Full hardware virtualization for predictable, dedicated performance.' },
{ icon: 'tabler-rocket', title: 'Near-Instant Provisioning', description: 'Your VPS is deployed seconds after ordering.' },
{ icon: 'tabler-refresh', title: 'Free ZFS Snapshots', description: 'Built-in snapshots for quick rollbacks. Off-site backup add-ons available.' },
{ icon: 'tabler-terminal', title: 'Full Root Access', description: 'Complete control over your server environment.' },
{ icon: 'tabler-server', title: 'VirtFusion Panel', description: 'Out-of-band console + VNC access included.' },
]
const includedFeatures: IncludedItem[] = [
{ text: '1 free IPv4 + 1 /64 IPv6 block' },
{ text: 'IPv4 rDNS / PTR control' },
{ text: '10 Gbps shared uplink (fair-use per AUP)' },
{ text: 'RAID 10 SSD storage' },
{ text: 'ZFS storage snapshots (free)' },
{ text: 'KVM virtualization' },
{ text: 'Full root access' },
{ text: 'Linux included; Windows supported (bring your own license)' },
{ text: 'VirtFusion control panel' },
{ text: 'Out-of-band console / VNC access' },
{ text: 'Near-instant provisioning' },
{ text: '99.9% uptime SLA' },
{ text: '14-day money-back guarantee' },
{ text: 'DDoS protection', comingSoon: true },
]
// Keys from features JSON that should not be shown as table columns
const internalKeys = new Set([
'control_panel',
'os',
'ipv4',
'ipv6',
'tier',
])
function getFeature(plan: EstimatorPlan, key: string): string {
return String(plan.features?.[key] ?? '-')
}
function formatPrice(plan: EstimatorPlan): string {
const price = parseFloat(plan.price) || 0
return price % 1 === 0 ? `$${price}` : `$${price.toFixed(2)}`
}
function prefillEstimator(planId: number): void {
estimator.setPlan(planId)
if (typeof window !== 'undefined') {
const url = estimator.shareUrl
window.history.replaceState({}, '', url)
// Smooth-scroll up to the estimator
const el = document.querySelector('.estimator-section')
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
}
</script>
<template>
<Head title="VPS Hosting | EZSCALE" />
<div>
<!-- Hero -->
<HeroSection>
<template #content>
<VChip color="primary" variant="tonal" class="mb-4">VPS Hosting</VChip>
<h1 class="text-h3 font-weight-bold mb-3">
Virtual Private <span class="hero-gradient-text">Servers</span>
</h1>
<p class="text-h6 text-medium-emphasis font-weight-regular mb-4" style="max-width: 600px;">
High-performance VPS hosting with RAID 10 SSD storage, dedicated resources, and full root access from our Atlanta, GA datacenter.
</p>
<p class="text-body-1 text-medium-emphasis mb-8">
Starting at just <span class="text-primary font-weight-bold">${{ startingPrice }}/mo</span>
</p>
<a :href="plans.length ? accountUrl + '/checkout/' + plans[0].id : '/pricing'" class="text-decoration-none">
<VBtn color="primary" size="large" rounded="lg">
Get Started
<VIcon icon="tabler-arrow-right" end />
</VBtn>
</a>
</template>
<template #visual>
<VpsHero />
</template>
</HeroSection>
<!-- Estimator -->
<EstimatorSection
:plans="plans"
:add-ons="addOns"
:workload-map="workloadMap"
:app-examples="appExamples"
:account-url="accountUrl"
/>
<!-- Features -->
<VContainer class="marketing-section">
<SectionHeader
label="Features"
title="Why Choose EZSCALE VPS?"
highlight-word="EZSCALE"
subtitle="Enterprise-grade infrastructure with the simplicity you deserve."
/>
<VRow>
<VCol v-for="(feature, index) in features" :key="feature.title" cols="12" sm="6" md="4">
<div :class="['d-flex ga-3 mb-4 feature-card-hover pa-3 rounded-lg', `fade-in-up stagger-${index + 1}`]">
<VAvatar color="primary" variant="tonal" size="44">
<VIcon :icon="feature.icon" size="22" />
</VAvatar>
<div>
<h3 class="text-subtitle-1 font-weight-bold">{{ feature.title }}</h3>
<p class="text-body-2 text-medium-emphasis mb-0">{{ feature.description }}</p>
</div>
</div>
</VCol>
</VRow>
</VContainer>
<!-- Plans Table -->
<div class="section-alt-bg marketing-section">
<VContainer>
<SectionHeader
label="Pricing"
title="VPS Plans"
highlight-word="Plans"
subtitle="All plans hosted in our Atlanta, GA datacenter. DDoS protection, full root access, and VirtFusion panel included."
/>
<VCard class="feature-card-hover">
<div style="overflow-x: auto; -webkit-overflow-scrolling: touch;">
<VTable>
<thead>
<tr>
<th>Plan</th>
<th>CPU</th>
<th>RAM</th>
<th>Storage</th>
<th>Bandwidth</th>
<th>Price</th>
<th />
</tr>
</thead>
<tbody>
<tr v-for="plan in plans" :key="plan.id">
<td class="font-weight-bold">{{ plan.name }}</td>
<td>{{ getFeature(plan, 'cpu') }}</td>
<td>{{ getFeature(plan, 'ram') }}</td>
<td>{{ getFeature(plan, 'storage') }}</td>
<td>{{ getFeature(plan, 'bandwidth') }}</td>
<td class="text-primary font-weight-bold">{{ formatPrice(plan) }}/mo</td>
<td>
<div class="d-flex align-center ga-2">
<a :href="accountUrl + '/checkout/' + plan.id" class="text-decoration-none">
<VBtn color="primary" size="small" variant="tonal">Order Now</VBtn>
</a>
<VBtn
size="small"
variant="text"
color="primary"
class="text-caption"
@click="prefillEstimator(plan.id)"
>
Estimate
</VBtn>
</div>
</td>
</tr>
</tbody>
</VTable>
</div>
</VCard>
<!-- Included with all plans -->
<VCard class="mt-8 pa-6 feature-card-hover">
<h3 class="text-h5 font-weight-bold mb-4 text-center">Included With All Plans</h3>
<VRow>
<VCol
v-for="item in includedFeatures"
:key="item.text"
cols="12"
sm="6"
md="4"
>
<div class="d-flex align-center ga-2 mb-2">
<VIcon
:icon="item.comingSoon ? 'tabler-clock' : 'tabler-circle-check'"
:color="item.comingSoon ? 'warning' : 'success'"
size="20"
/>
<span class="text-body-1">{{ item.text }}</span>
<VChip
v-if="item.comingSoon"
size="x-small"
color="warning"
variant="tonal"
density="compact"
class="ms-1"
>
Coming soon
</VChip>
</div>
</VCol>
</VRow>
</VCard>
</VContainer>
</div>
<!-- CTA -->
<div class="marketing-section" style="background: linear-gradient(135deg, rgb(var(--v-theme-primary), 0.12), rgb(var(--v-theme-surface)));">
<VContainer class="text-center">
<h2 class="text-h4 font-weight-bold mb-3">Ready to Get Started?</h2>
<p class="text-body-1 text-medium-emphasis mb-6">
Deploy your VPS in seconds with full root access, DDoS protection, and expert support.
</p>
<div class="d-flex ga-3 justify-center flex-wrap">
<a :href="plans.length ? accountUrl + '/checkout/' + plans[0].id : '/pricing'" class="text-decoration-none">
<VBtn color="primary" size="large" rounded="lg">
Get Started
<VIcon icon="tabler-arrow-right" end />
</VBtn>
</a>
<a href="/contact" class="text-decoration-none">
<VBtn color="primary" variant="outlined" size="large" rounded="lg">
Contact Sales
</VBtn>
</a>
</div>
</VContainer>
</div>
</div>
</template>