Migrate frontend to Vuetify/Vuexy + add real WHMCS product data

- Migrate all frontend from plain JS/Tailwind to TypeScript/Vuetify 3 (Vuexy design system)
- Replace placeholder plans with 25 real products scraped from WHMCS:
  9 VPS plans ($4.20-$30/mo), 8 dedicated servers ($44.39-$107.99/mo),
  4 web hosting plans ($2.39-$15.99/mo), 4 MySQL hosting plans ($6-$30/mo)
- Fix Pricing page: correct field mapping (service_type, price), display
  feature values instead of keys, proper price formatting
- Update all marketing pages (Home, Products, VPS, Dedicated, Web Hosting)
  with real specs, pricing, and features from production WHMCS
- Add 38 Vuexy @core SCSS override files for component styling
- Create 4 layouts (Account, Admin, Auth, Marketing) with Vuetify
- Add AppTextField/AppSelect/AppTextarea wrapper components
- Purple primary theme (#7367F0), dark mode default
- 52 tests passing, build clean

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude Dev
2026-02-09 10:16:41 -05:00
parent 0fe4e4ab42
commit ec8f0272ec
141 changed files with 9592 additions and 2440 deletions

View File

@@ -0,0 +1,49 @@
<script lang="ts" setup>
import AdminLayout from '@/Layouts/AdminLayout.vue'
import StatCard from '@/Components/StatCard.vue'
interface Props {
totalCustomers: number
totalServices: number
activeServices: number
}
defineOptions({ layout: AdminLayout })
defineProps<Props>()
</script>
<template>
<div>
<div class="text-h4 font-weight-bold mb-6">Admin Dashboard</div>
<VRow>
<VCol cols="12" md="4">
<StatCard
title="Total Customers"
:stats="totalCustomers"
icon="tabler-users"
color="primary"
/>
</VCol>
<VCol cols="12" md="4">
<StatCard
title="Total Services"
:stats="totalServices"
icon="tabler-server"
color="info"
/>
</VCol>
<VCol cols="12" md="4">
<StatCard
title="Active Services"
:stats="activeServices"
icon="tabler-circle-check"
color="success"
/>
</VCol>
</VRow>
</div>
</template>

View File

@@ -0,0 +1,57 @@
<script lang="ts" setup>
import { useForm } from '@inertiajs/vue3'
import AuthLayout from '@/Layouts/AuthLayout.vue'
import AppTextField from '@/Components/app-form-elements/AppTextField.vue'
defineOptions({ layout: AuthLayout })
const form = useForm({
password: '',
})
const submit = (): void => {
form.post('/user/confirm-password', {
onFinish: () => form.reset('password'),
})
}
</script>
<template>
<VCardText>
<h4 class="text-h4 mb-1">
Confirm your password
</h4>
<p class="mb-0">
Please confirm your password before continuing
</p>
</VCardText>
<VCardText>
<VForm @submit.prevent="submit">
<VRow>
<VCol cols="12">
<AppTextField
v-model="form.password"
label="Password"
placeholder="············"
type="password"
required
autofocus
:error-messages="form.errors.password ? [form.errors.password] : []"
/>
</VCol>
<VCol cols="12">
<VBtn
type="submit"
block
:loading="form.processing"
:disabled="form.processing"
>
Confirm
</VBtn>
</VCol>
</VRow>
</VForm>
</VCardText>
</template>

View File

@@ -0,0 +1,84 @@
<script lang="ts" setup>
import { useForm } from '@inertiajs/vue3'
import AuthLayout from '@/Layouts/AuthLayout.vue'
import AppTextField from '@/Components/app-form-elements/AppTextField.vue'
interface Props {
status?: string
}
defineOptions({ layout: AuthLayout })
defineProps<Props>()
const form = useForm({
email: '',
})
const submit = (): void => {
form.post('/forgot-password')
}
</script>
<template>
<VCardText>
<h4 class="text-h4 mb-1">
Forgot Password?
</h4>
<p class="mb-0">
Enter your email and we'll send you a reset link
</p>
</VCardText>
<VCardText>
<VAlert
v-if="status"
type="success"
variant="tonal"
class="mb-4"
>
{{ status }}
</VAlert>
<VForm @submit.prevent="submit">
<VRow>
<VCol cols="12">
<AppTextField
v-model="form.email"
label="Email"
type="email"
required
autofocus
placeholder="john@example.com"
:error-messages="form.errors.email ? [form.errors.email] : []"
/>
</VCol>
<VCol cols="12">
<VBtn
type="submit"
block
:loading="form.processing"
:disabled="form.processing"
>
Send Reset Link
</VBtn>
</VCol>
<VCol cols="12">
<a
href="/login"
class="d-flex align-center justify-center"
>
<VIcon
icon="tabler-chevron-left"
size="20"
class="me-1"
/>
<span>Back to login</span>
</a>
</VCol>
</VRow>
</VForm>
</VCardText>
</template>

View File

@@ -0,0 +1,118 @@
<script lang="ts" setup>
import { useForm, Link } from '@inertiajs/vue3'
import { ref } from 'vue'
import AuthLayout from '@/Layouts/AuthLayout.vue'
import AppTextField from '@/Components/app-form-elements/AppTextField.vue'
defineOptions({ layout: AuthLayout })
interface Props {
status?: string
}
defineProps<Props>()
const isPasswordVisible = ref(false)
const form = useForm({
email: '',
password: '',
remember: false,
})
const submit = (): void => {
form.post('/login', {
onFinish: () => form.reset('password'),
})
}
</script>
<template>
<VCardText>
<h4 class="text-h4 mb-1">
Welcome to <span class="text-capitalize">EZSCALE</span>!
</h4>
<p class="mb-0">
Please sign-in to your account and start the adventure
</p>
</VCardText>
<VCardText>
<VAlert
v-if="status"
type="success"
variant="tonal"
class="mb-4"
>
{{ status }}
</VAlert>
<VForm @submit.prevent="submit">
<VRow>
<!-- email -->
<VCol cols="12">
<AppTextField
v-model="form.email"
autofocus
label="Email"
type="email"
placeholder="john@example.com"
:error-messages="form.errors.email ? [form.errors.email] : []"
/>
</VCol>
<!-- password -->
<VCol cols="12">
<AppTextField
v-model="form.password"
label="Password"
placeholder="············"
:type="isPasswordVisible ? 'text' : 'password'"
autocomplete="current-password"
:append-inner-icon="isPasswordVisible ? 'tabler-eye-off' : 'tabler-eye'"
:error-messages="form.errors.password ? [form.errors.password] : []"
@click:append-inner="isPasswordVisible = !isPasswordVisible"
/>
<div class="d-flex align-center flex-wrap justify-space-between my-6">
<VCheckbox
v-model="form.remember"
label="Remember me"
/>
<Link
class="text-primary"
href="/forgot-password"
>
Forgot Password?
</Link>
</div>
<VBtn
block
type="submit"
:loading="form.processing"
:disabled="form.processing"
>
Login
</VBtn>
</VCol>
<!-- create account -->
<VCol
cols="12"
class="text-body-1 text-center"
>
<span class="d-inline-block">
New on our platform?
</span>
<Link
class="text-primary ms-1 d-inline-block text-body-1"
href="/register"
>
Create an account
</Link>
</VCol>
</VRow>
</VForm>
</VCardText>
</template>

View File

@@ -0,0 +1,129 @@
<script lang="ts" setup>
import { useForm, Link } from '@inertiajs/vue3'
import { ref } from 'vue'
import AuthLayout from '@/Layouts/AuthLayout.vue'
import AppTextField from '@/Components/app-form-elements/AppTextField.vue'
defineOptions({ layout: AuthLayout })
const isPasswordVisible = ref(false)
const privacyPolicy = ref(false)
const form = useForm({
name: '',
email: '',
password: '',
password_confirmation: '',
})
const submit = (): void => {
form.post('/register', {
onFinish: () => form.reset('password', 'password_confirmation'),
})
}
</script>
<template>
<VCardText>
<h4 class="text-h4 mb-1">
Adventure starts here
</h4>
<p class="mb-0">
Start hosting your projects with EZSCALE
</p>
</VCardText>
<VCardText>
<VForm @submit.prevent="submit">
<VRow>
<!-- name -->
<VCol cols="12">
<AppTextField
v-model="form.name"
autofocus
label="Full Name"
placeholder="John Doe"
:error-messages="form.errors.name ? [form.errors.name] : []"
/>
</VCol>
<!-- email -->
<VCol cols="12">
<AppTextField
v-model="form.email"
label="Email"
type="email"
placeholder="john@example.com"
:error-messages="form.errors.email ? [form.errors.email] : []"
/>
</VCol>
<!-- password -->
<VCol cols="12">
<AppTextField
v-model="form.password"
label="Password"
placeholder="············"
:type="isPasswordVisible ? 'text' : 'password'"
autocomplete="new-password"
:append-inner-icon="isPasswordVisible ? 'tabler-eye-off' : 'tabler-eye'"
:error-messages="form.errors.password ? [form.errors.password] : []"
@click:append-inner="isPasswordVisible = !isPasswordVisible"
/>
</VCol>
<!-- confirm password -->
<VCol cols="12">
<AppTextField
v-model="form.password_confirmation"
label="Confirm Password"
placeholder="············"
:type="isPasswordVisible ? 'text' : 'password'"
autocomplete="new-password"
:append-inner-icon="isPasswordVisible ? 'tabler-eye-off' : 'tabler-eye'"
@click:append-inner="isPasswordVisible = !isPasswordVisible"
/>
<div class="d-flex align-center my-6">
<VCheckbox
v-model="privacyPolicy"
inline
>
<template #label>
<span class="me-1">I agree to the</span>
<a href="/terms" class="text-primary" target="_blank">Terms of Service</a>
<span class="mx-1">&amp;</span>
<a href="/privacy" class="text-primary" target="_blank">Privacy Policy</a>
</template>
</VCheckbox>
</div>
<VBtn
block
type="submit"
:loading="form.processing"
:disabled="form.processing || !privacyPolicy"
>
Sign Up
</VBtn>
</VCol>
<!-- login link -->
<VCol
cols="12"
class="text-body-1 text-center"
>
<span class="d-inline-block">
Already have an account?
</span>
<Link
class="text-primary ms-1 d-inline-block text-body-1"
href="/login"
>
Sign in instead
</Link>
</VCol>
</VRow>
</VForm>
</VCardText>
</template>

View File

@@ -0,0 +1,94 @@
<script lang="ts" setup>
import { useForm } from '@inertiajs/vue3'
import { ref } from 'vue'
import AuthLayout from '@/Layouts/AuthLayout.vue'
import AppTextField from '@/Components/app-form-elements/AppTextField.vue'
interface Props {
token: string
email: string
}
defineOptions({ layout: AuthLayout })
const props = defineProps<Props>()
const isPasswordVisible = ref(false)
const form = useForm({
token: props.token,
email: props.email,
password: '',
password_confirmation: '',
})
const submit = (): void => {
form.post('/reset-password', {
onFinish: () => form.reset('password', 'password_confirmation'),
})
}
</script>
<template>
<VCardText>
<h4 class="text-h4 mb-1">
Set new password
</h4>
<p class="mb-0">
Your new password must be different from previously used passwords
</p>
</VCardText>
<VCardText>
<VForm @submit.prevent="submit">
<VRow>
<VCol cols="12">
<AppTextField
v-model="form.email"
label="Email"
type="email"
required
placeholder="john@example.com"
:error-messages="form.errors.email ? [form.errors.email] : []"
/>
</VCol>
<VCol cols="12">
<AppTextField
v-model="form.password"
label="New Password"
placeholder="············"
:type="isPasswordVisible ? 'text' : 'password'"
required
:append-inner-icon="isPasswordVisible ? 'tabler-eye-off' : 'tabler-eye'"
:error-messages="form.errors.password ? [form.errors.password] : []"
@click:append-inner="isPasswordVisible = !isPasswordVisible"
/>
</VCol>
<VCol cols="12">
<AppTextField
v-model="form.password_confirmation"
label="Confirm Password"
placeholder="············"
:type="isPasswordVisible ? 'text' : 'password'"
required
:append-inner-icon="isPasswordVisible ? 'tabler-eye-off' : 'tabler-eye'"
@click:append-inner="isPasswordVisible = !isPasswordVisible"
/>
</VCol>
<VCol cols="12">
<VBtn
type="submit"
block
:loading="form.processing"
:disabled="form.processing"
>
Reset password
</VBtn>
</VCol>
</VRow>
</VForm>
</VCardText>
</template>

View File

@@ -0,0 +1,93 @@
<script lang="ts" setup>
import { ref } from 'vue'
import { useForm } from '@inertiajs/vue3'
import AuthLayout from '@/Layouts/AuthLayout.vue'
import AppTextField from '@/Components/app-form-elements/AppTextField.vue'
defineOptions({ layout: AuthLayout })
const useRecovery = ref(false)
const form = useForm({
code: '',
recovery_code: '',
})
const submit = (): void => {
form.post('/two-factor-challenge', {
onFinish: () => form.reset(),
})
}
const toggleRecovery = (): void => {
useRecovery.value = !useRecovery.value
form.reset()
}
</script>
<template>
<VCardText>
<h4 class="text-h4 mb-1">
Two-Factor Authentication
</h4>
<p class="mb-0">
<template v-if="!useRecovery">
Enter the authentication code from your authenticator app
</template>
<template v-else>
Enter one of your emergency recovery codes
</template>
</p>
</VCardText>
<VCardText>
<VForm @submit.prevent="submit">
<VRow>
<VCol v-if="!useRecovery" cols="12">
<AppTextField
v-model="form.code"
label="Code"
type="text"
inputmode="numeric"
autofocus
autocomplete="one-time-code"
placeholder="000000"
:error-messages="form.errors.code ? [form.errors.code] : []"
/>
</VCol>
<VCol v-else cols="12">
<AppTextField
v-model="form.recovery_code"
label="Recovery Code"
type="text"
autofocus
placeholder="Enter recovery code"
:error-messages="form.errors.recovery_code ? [form.errors.recovery_code] : []"
/>
</VCol>
<VCol cols="12">
<VBtn
type="submit"
block
:loading="form.processing"
:disabled="form.processing"
>
Verify
</VBtn>
</VCol>
<VCol cols="12">
<VBtn
variant="text"
block
@click="toggleRecovery"
>
{{ useRecovery ? 'Use authentication code' : 'Use a recovery code' }}
</VBtn>
</VCol>
</VRow>
</VForm>
</VCardText>
</template>

View File

@@ -0,0 +1,51 @@
<script lang="ts" setup>
import { useForm } from '@inertiajs/vue3'
import AuthLayout from '@/Layouts/AuthLayout.vue'
interface Props {
status?: string
}
defineOptions({ layout: AuthLayout })
defineProps<Props>()
const form = useForm({})
const submit = (): void => {
form.post('/email/verification-notification')
}
</script>
<template>
<VCardText>
<h4 class="text-h4 mb-1">
Verify your email
</h4>
<p class="mb-0">
We've sent a verification link to your email. Please check your inbox and click the link to verify.
</p>
</VCardText>
<VCardText>
<VAlert
v-if="status === 'verification-link-sent'"
type="success"
variant="tonal"
class="mb-4"
>
A new verification link has been sent to your email address.
</VAlert>
<VForm @submit.prevent="submit">
<VBtn
type="submit"
block
:loading="form.processing"
:disabled="form.processing"
>
Resend verification email
</VBtn>
</VForm>
</VCardText>
</template>

View File

@@ -0,0 +1,168 @@
<script lang="ts" setup>
import { useForm, Link } from '@inertiajs/vue3'
import AccountLayout from '@/Layouts/AccountLayout.vue'
import { resolveInvoiceStatusColor, resolveTransactionStatusColor } from '@/utils/resolvers'
import type { PaymentMethod, Invoice, Transaction } from '@/types'
interface Props {
paymentMethods: PaymentMethod[]
invoices: Invoice[]
transactions: Transaction[]
intent: Record<string, unknown>
stripeKey: string
}
defineOptions({ layout: AccountLayout })
defineProps<Props>()
const defaultForm = useForm({
payment_method_id: '',
})
const setDefault = (id: string): void => {
defaultForm.payment_method_id = id
defaultForm.post('/billing/payment-methods/default')
}
const removeMethod = (id: string): void => {
if (confirm('Are you sure you want to remove this payment method?')) {
useForm({}).delete(`/billing/payment-methods/${id}`)
}
}
</script>
<template>
<div>
<div class="text-h4 font-weight-bold mb-6">Billing</div>
<!-- Payment Methods -->
<VCard class="mb-6">
<VCardTitle>Payment Methods</VCardTitle>
<VCardText>
<div v-if="paymentMethods.length === 0" class="text-body-2 text-medium-emphasis">
No payment methods on file.
</div>
<div v-else class="d-flex flex-column ga-3">
<VSheet
v-for="pm in paymentMethods"
:key="pm.id"
rounded
border
class="pa-3 d-flex align-center justify-space-between"
:class="pm.is_default ? 'border-primary' : ''"
>
<div class="d-flex align-center ga-3">
<span class="text-body-2 font-weight-medium text-capitalize">{{ pm.brand }}</span>
<span class="text-body-2 text-medium-emphasis">&bull;&bull;&bull;&bull; {{ pm.last_four }}</span>
<span class="text-body-2 text-disabled">{{ pm.exp_month }}/{{ pm.exp_year }}</span>
<VChip v-if="pm.is_default" color="primary" size="x-small">Default</VChip>
</div>
<div class="d-flex align-center ga-2">
<VBtn
v-if="!pm.is_default"
variant="text"
size="small"
:disabled="defaultForm.processing"
@click="setDefault(pm.id)"
>
Make Default
</VBtn>
<VBtn
variant="text"
color="error"
size="small"
@click="removeMethod(pm.id)"
>
Remove
</VBtn>
</div>
</VSheet>
</div>
</VCardText>
</VCard>
<!-- Recent Invoices -->
<VCard class="mb-6">
<VCardTitle class="d-flex align-center justify-space-between">
<span>Recent Invoices</span>
<Link href="/billing/invoices" class="text-primary text-body-2 text-decoration-none">View All</Link>
</VCardTitle>
<VCardText>
<div v-if="invoices.length === 0" class="text-body-2 text-medium-emphasis">No invoices yet.</div>
<VTable v-else>
<thead>
<tr>
<th>Number</th>
<th>Date</th>
<th>Status</th>
<th class="text-end">Amount</th>
<th class="text-end" />
</tr>
</thead>
<tbody>
<tr v-for="invoice in invoices" :key="invoice.id">
<td>{{ invoice.number }}</td>
<td>{{ new Date(invoice.created_at).toLocaleDateString() }}</td>
<td>
<VChip
:color="resolveInvoiceStatusColor(invoice.status)"
size="small"
class="text-capitalize"
>
{{ invoice.status }}
</VChip>
</td>
<td class="text-end">${{ parseFloat(invoice.total).toFixed(2) }}</td>
<td class="text-end">
<a :href="`/billing/invoices/${invoice.id}/download`" class="text-primary text-decoration-none">Download</a>
</td>
</tr>
</tbody>
</VTable>
</VCardText>
</VCard>
<!-- Recent Transactions -->
<VCard>
<VCardTitle class="d-flex align-center justify-space-between">
<span>Recent Transactions</span>
<Link href="/billing/transactions" class="text-primary text-body-2 text-decoration-none">View All</Link>
</VCardTitle>
<VCardText>
<div v-if="transactions.length === 0" class="text-body-2 text-medium-emphasis">No transactions yet.</div>
<VTable v-else>
<thead>
<tr>
<th>Date</th>
<th>Gateway</th>
<th>Status</th>
<th>Description</th>
<th class="text-end">Amount</th>
</tr>
</thead>
<tbody>
<tr v-for="tx in transactions" :key="tx.id">
<td>{{ new Date(tx.created_at).toLocaleDateString() }}</td>
<td class="text-capitalize">{{ tx.gateway }}</td>
<td>
<VChip
:color="resolveTransactionStatusColor(tx.status)"
size="small"
class="text-capitalize"
>
{{ tx.status }}
</VChip>
</td>
<td>{{ tx.description }}</td>
<td class="text-end">${{ parseFloat(tx.amount).toFixed(2) }}</td>
</tr>
</tbody>
</VTable>
</VCardText>
</VCard>
</div>
</template>

View File

@@ -0,0 +1,88 @@
<script lang="ts" setup>
import { Link } from '@inertiajs/vue3'
import AccountLayout from '@/Layouts/AccountLayout.vue'
import { resolveInvoiceStatusColor } from '@/utils/resolvers'
import type { Invoice, PaginatedResponse } from '@/types'
interface Props {
invoices: PaginatedResponse<Invoice>
}
defineOptions({ layout: AccountLayout })
defineProps<Props>()
</script>
<template>
<div>
<div class="mb-4">
<Link href="/billing" class="text-primary text-body-2 text-decoration-none">&larr; Back to Billing</Link>
</div>
<div class="text-h4 font-weight-bold mb-6">Invoices</div>
<VCard>
<VCardText v-if="!invoices.data || invoices.data.length === 0" class="text-center py-6">
<div class="text-medium-emphasis">No invoices found.</div>
</VCardText>
<template v-else>
<VTable>
<thead>
<tr>
<th>Number</th>
<th>Date</th>
<th>Gateway</th>
<th>Status</th>
<th class="text-end">Amount</th>
<th class="text-end" />
</tr>
</thead>
<tbody>
<tr v-for="invoice in invoices.data" :key="invoice.id">
<td>{{ invoice.number }}</td>
<td>{{ new Date(invoice.created_at).toLocaleDateString() }}</td>
<td class="text-capitalize">{{ invoice.gateway }}</td>
<td>
<VChip
:color="resolveInvoiceStatusColor(invoice.status)"
size="small"
class="text-capitalize"
>
{{ invoice.status }}
</VChip>
</td>
<td class="text-end">${{ parseFloat(invoice.total).toFixed(2) }}</td>
<td class="text-end">
<a :href="`/billing/invoices/${invoice.id}/download`" class="text-primary text-decoration-none">Download</a>
</td>
</tr>
</tbody>
</VTable>
<!-- Pagination -->
<div v-if="invoices.links && invoices.last_page > 1" class="d-flex align-center justify-space-between pa-4">
<div class="text-body-2 text-medium-emphasis">
Showing {{ invoices.from }} to {{ invoices.to }} of {{ invoices.total }}
</div>
<div class="d-flex ga-1">
<Link
v-for="link in invoices.links"
:key="link.label"
:href="link.url || '#'"
class="text-decoration-none"
>
<VBtn
size="small"
:color="link.active ? 'primary' : undefined"
:variant="link.active ? 'flat' : 'text'"
:disabled="!link.url"
v-html="link.label"
/>
</Link>
</div>
</div>
</template>
</VCard>
</div>
</template>

View File

@@ -0,0 +1,86 @@
<script lang="ts" setup>
import { Link } from '@inertiajs/vue3'
import AccountLayout from '@/Layouts/AccountLayout.vue'
import { resolveTransactionStatusColor } from '@/utils/resolvers'
import type { Transaction, PaginatedResponse } from '@/types'
interface Props {
transactions: PaginatedResponse<Transaction>
}
defineOptions({ layout: AccountLayout })
defineProps<Props>()
</script>
<template>
<div>
<div class="mb-4">
<Link href="/billing" class="text-primary text-body-2 text-decoration-none">&larr; Back to Billing</Link>
</div>
<div class="text-h4 font-weight-bold mb-6">Transactions</div>
<VCard>
<VCardText v-if="!transactions.data || transactions.data.length === 0" class="text-center py-6">
<div class="text-medium-emphasis">No transactions found.</div>
</VCardText>
<template v-else>
<VTable>
<thead>
<tr>
<th>Date</th>
<th>Gateway</th>
<th>Method</th>
<th>Status</th>
<th>Description</th>
<th class="text-end">Amount</th>
</tr>
</thead>
<tbody>
<tr v-for="tx in transactions.data" :key="tx.id">
<td>{{ new Date(tx.created_at).toLocaleDateString() }}</td>
<td class="text-capitalize">{{ tx.gateway }}</td>
<td class="text-capitalize">{{ tx.payment_method }}</td>
<td>
<VChip
:color="resolveTransactionStatusColor(tx.status)"
size="small"
class="text-capitalize"
>
{{ tx.status }}
</VChip>
</td>
<td>{{ tx.description }}</td>
<td class="text-end">${{ parseFloat(tx.amount).toFixed(2) }}</td>
</tr>
</tbody>
</VTable>
<!-- Pagination -->
<div v-if="transactions.links && transactions.last_page > 1" class="d-flex align-center justify-space-between pa-4">
<div class="text-body-2 text-medium-emphasis">
Showing {{ transactions.from }} to {{ transactions.to }} of {{ transactions.total }}
</div>
<div class="d-flex ga-1">
<Link
v-for="link in transactions.links"
:key="link.label"
:href="link.url || '#'"
class="text-decoration-none"
>
<VBtn
size="small"
:color="link.active ? 'primary' : undefined"
:variant="link.active ? 'flat' : 'text'"
:disabled="!link.url"
v-html="link.label"
/>
</Link>
</div>
</div>
</template>
</VCard>
</div>
</template>

View File

@@ -0,0 +1,199 @@
<script lang="ts" setup>
import { ref, computed } from 'vue'
import { useForm, Link } from '@inertiajs/vue3'
import AccountLayout from '@/Layouts/AccountLayout.vue'
import AppTextField from '@/Components/app-form-elements/AppTextField.vue'
import AppSelect from '@/Components/app-form-elements/AppSelect.vue'
import type { Plan, PaymentMethod } from '@/types'
interface Props {
plan: Plan
paymentMethods: PaymentMethod[]
intent: Record<string, unknown>
stripeKey: string
}
defineOptions({ layout: AccountLayout })
const props = defineProps<Props>()
const selectedGateway = ref('stripe')
const selectedPaymentMethod = ref(props.paymentMethods?.[0]?.id || '')
const couponCode = ref('')
const couponApplied = ref(false)
const couponDiscount = ref(0)
const couponError = ref('')
const total = computed(() => {
const price = parseFloat(props.plan.price)
return Math.max(0, price - couponDiscount.value).toFixed(2)
})
const form = useForm({
gateway: 'stripe',
payment_method_id: props.paymentMethods?.[0]?.id || '',
coupon_code: '',
})
const applyCoupon = async (): Promise<void> => {
couponError.value = ''
couponApplied.value = false
try {
const response = await fetch('/checkout/apply-coupon', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '',
'Accept': 'application/json',
},
body: JSON.stringify({
code: couponCode.value,
plan_id: props.plan.id,
}),
})
const data = await response.json()
if (data.valid) {
couponApplied.value = true
couponDiscount.value = data.discount
} else {
couponError.value = data.message || 'Invalid coupon.'
}
} catch {
couponError.value = 'Failed to validate coupon.'
}
}
const submit = (): void => {
form.gateway = selectedGateway.value
form.payment_method_id = selectedPaymentMethod.value
form.coupon_code = couponApplied.value ? couponCode.value : ''
form.post(`/checkout/${props.plan.id}`)
}
</script>
<template>
<div>
<div class="mb-4">
<Link href="/plans" class="text-primary text-body-2 text-decoration-none">&larr; Back to Plans</Link>
</div>
<div class="text-h4 font-weight-bold mb-6">Checkout</div>
<VRow>
<!-- Order Summary -->
<VCol cols="12" lg="4" order="2" order-lg="1">
<VCard>
<VCardTitle>Order Summary</VCardTitle>
<VCardText>
<div class="d-flex justify-space-between text-body-2 mb-2">
<span class="text-medium-emphasis">{{ plan.name }}</span>
<span>${{ parseFloat(plan.price).toFixed(2) }}</span>
</div>
<div class="d-flex justify-space-between text-body-2 text-medium-emphasis mb-2">
<span>Billing Cycle</span>
<span class="text-capitalize">{{ plan.billing_cycle }}</span>
</div>
<div v-if="couponApplied" class="d-flex justify-space-between text-body-2 text-success mb-2">
<span>Discount</span>
<span>-${{ couponDiscount.toFixed(2) }}</span>
</div>
<VDivider class="my-3" />
<div class="d-flex justify-space-between font-weight-bold">
<span>Total</span>
<span>${{ total }}/{{ plan.billing_cycle }}</span>
</div>
</VCardText>
</VCard>
</VCol>
<!-- Checkout Form -->
<VCol cols="12" lg="8" order="1" order-lg="2">
<VForm @submit.prevent="submit">
<!-- Payment Gateway -->
<VCard class="mb-6">
<VCardTitle>Payment Method</VCardTitle>
<VCardText>
<VRadioGroup v-model="selectedGateway" hide-details>
<VRadio label="Credit / Debit Card (Stripe)" value="stripe" />
<VRadio label="PayPal" value="paypal" />
</VRadioGroup>
<!-- Saved Payment Methods (Stripe) -->
<div v-if="selectedGateway === 'stripe' && paymentMethods.length > 0" class="mt-4">
<AppSelect
v-model="selectedPaymentMethod"
label="Select Card"
:items="paymentMethods.map(pm => ({
title: `${pm.brand} ending in ${pm.last_four} (${pm.exp_month}/${pm.exp_year})${pm.is_default ? ' - Default' : ''}`,
value: pm.id,
}))"
/>
</div>
<div v-if="selectedGateway === 'stripe' && paymentMethods.length === 0" class="mt-4">
<div class="text-body-2 text-medium-emphasis">
You have no saved payment methods.
<Link href="/billing" class="text-primary text-decoration-none">Add one first</Link>.
</div>
</div>
</VCardText>
</VCard>
<!-- Coupon -->
<VCard class="mb-6">
<VCardTitle>Coupon Code</VCardTitle>
<VCardText>
<div class="d-flex ga-3">
<AppTextField
v-model="couponCode"
placeholder="Enter coupon code"
:disabled="couponApplied"
hide-details
density="compact"
class="flex-grow-1"
/>
<VBtn
variant="outlined"
:disabled="!couponCode || couponApplied"
@click="applyCoupon"
>
{{ couponApplied ? 'Applied' : 'Apply' }}
</VBtn>
</div>
<div v-if="couponError" class="text-body-2 text-error mt-2">{{ couponError }}</div>
<div v-if="couponApplied" class="text-body-2 text-success mt-2">Coupon applied successfully!</div>
</VCardText>
</VCard>
<!-- Errors -->
<VAlert
v-if="form.errors && Object.keys(form.errors).length"
type="error"
variant="tonal"
class="mb-6"
>
<ul class="ps-4">
<li v-for="(error, field) in form.errors" :key="field">{{ error }}</li>
</ul>
</VAlert>
<!-- Submit -->
<VBtn
type="submit"
block
size="large"
:loading="form.processing"
:disabled="form.processing || (selectedGateway === 'stripe' && !selectedPaymentMethod)"
>
<span v-if="form.processing">Processing...</span>
<span v-else>Subscribe for ${{ total }}/{{ plan.billing_cycle }}</span>
</VBtn>
</VForm>
</VCol>
</VRow>
</div>
</template>

View File

@@ -0,0 +1,73 @@
<script lang="ts" setup>
import { Link } from '@inertiajs/vue3'
import AccountLayout from '@/Layouts/AccountLayout.vue'
import StatCard from '@/Components/StatCard.vue'
interface Props {
servicesCount: number
activeServicesCount: number
}
defineOptions({ layout: AccountLayout })
defineProps<Props>()
</script>
<template>
<div>
<div class="text-h4 font-weight-bold mb-6">Dashboard</div>
<VRow>
<VCol cols="12" md="4">
<StatCard
title="Total Services"
:stats="servicesCount"
icon="tabler-server"
color="primary"
/>
</VCol>
<VCol cols="12" md="4">
<StatCard
title="Active Services"
:stats="activeServicesCount"
icon="tabler-circle-check"
color="success"
/>
</VCol>
<VCol cols="12" md="4">
<VCard>
<VCardText>
<div class="d-flex align-center gap-4">
<VAvatar color="info" variant="tonal" rounded size="44">
<VIcon icon="tabler-bolt" size="26" />
</VAvatar>
<div>
<div class="text-body-2 text-medium-emphasis">Quick Actions</div>
</div>
</div>
<div class="mt-4 d-flex flex-column ga-2">
<Link href="/plans" class="text-primary text-body-2 text-decoration-none d-flex align-center ga-1">
<VIcon icon="tabler-chevron-right" size="16" />
Browse Plans
</Link>
<Link href="/subscriptions" class="text-primary text-body-2 text-decoration-none d-flex align-center ga-1">
<VIcon icon="tabler-chevron-right" size="16" />
My Subscriptions
</Link>
<Link href="/billing" class="text-primary text-body-2 text-decoration-none d-flex align-center ga-1">
<VIcon icon="tabler-chevron-right" size="16" />
Billing &amp; Payments
</Link>
<Link href="/profile" class="text-primary text-body-2 text-decoration-none d-flex align-center ga-1">
<VIcon icon="tabler-chevron-right" size="16" />
Edit Profile
</Link>
</div>
</VCardText>
</VCard>
</VCol>
</VRow>
</div>
</template>

View File

@@ -0,0 +1,108 @@
<script lang="ts" setup>
import MarketingLayout from '@/Layouts/MarketingLayout.vue'
defineOptions({ layout: MarketingLayout })
const values = [
{ icon: 'tabler-heart', title: 'Customer First', description: 'Every decision we make starts with what\'s best for our customers.', color: 'error' },
{ icon: 'tabler-shield-check', title: 'Reliability', description: 'We engineer our infrastructure for maximum uptime and performance.', color: 'success' },
{ icon: 'tabler-eye', title: 'Transparency', description: 'No hidden fees, no surprise charges. What you see is what you pay.', color: 'primary' },
{ icon: 'tabler-rocket', title: 'Innovation', description: 'We continuously invest in the latest hardware and software technologies.', color: 'warning' },
]
const milestones = [
{ year: '2024', event: 'EZSCALE founded with a mission to simplify cloud hosting.' },
{ year: '2024', event: 'Launched VPS and Dedicated Server product lines.' },
{ year: '2025', event: 'Expanded to 50+ data center locations worldwide.' },
{ year: '2025', event: 'Surpassed 10,000 active customers.' },
]
</script>
<template>
<div>
<!-- Hero -->
<VContainer class="py-16">
<VRow align="center">
<VCol cols="12" md="8" class="mx-auto text-center">
<h1 class="text-h2 font-weight-bold mb-4">About EZSCALE</h1>
<p class="text-h6 text-medium-emphasis font-weight-regular">
We're on a mission to make cloud hosting simple, affordable, and accessible to everyone.
From individual developers to growing businesses, EZSCALE provides the infrastructure you need to succeed.
</p>
</VCol>
</VRow>
</VContainer>
<!-- Values -->
<div class="bg-surface-variant py-16">
<VContainer>
<div class="text-center mb-12">
<h2 class="text-h3 font-weight-bold mb-3">Our Values</h2>
</div>
<VRow>
<VCol v-for="value in values" :key="value.title" cols="12" sm="6" md="3">
<VCard variant="flat" class="text-center pa-6 h-100 bg-transparent">
<VAvatar :color="value.color" variant="tonal" size="64" class="mb-4">
<VIcon :icon="value.icon" size="32" />
</VAvatar>
<h3 class="text-h6 font-weight-bold mb-2">{{ value.title }}</h3>
<p class="text-body-2 text-medium-emphasis mb-0">{{ value.description }}</p>
</VCard>
</VCol>
</VRow>
</VContainer>
</div>
<!-- Timeline -->
<VContainer class="py-16">
<div class="text-center mb-12">
<h2 class="text-h3 font-weight-bold mb-3">Our Journey</h2>
</div>
<VRow justify="center">
<VCol cols="12" md="8">
<VTimeline side="end" density="compact">
<VTimelineItem
v-for="(milestone, index) in milestones"
:key="index"
dot-color="primary"
size="small"
>
<VCard variant="outlined">
<VCardText>
<div class="text-caption text-primary font-weight-bold mb-1">{{ milestone.year }}</div>
<div class="text-body-1">{{ milestone.event }}</div>
</VCardText>
</VCard>
</VTimelineItem>
</VTimeline>
</VCol>
</VRow>
</VContainer>
<!-- Stats -->
<div class="bg-surface-variant py-16">
<VContainer>
<VRow>
<VCol cols="6" md="3" class="text-center">
<div class="text-h3 font-weight-bold text-primary">10K+</div>
<div class="text-body-1 text-medium-emphasis mt-1">Customers</div>
</VCol>
<VCol cols="6" md="3" class="text-center">
<div class="text-h3 font-weight-bold text-primary">99.99%</div>
<div class="text-body-1 text-medium-emphasis mt-1">Uptime</div>
</VCol>
<VCol cols="6" md="3" class="text-center">
<div class="text-h3 font-weight-bold text-primary">50+</div>
<div class="text-body-1 text-medium-emphasis mt-1">Locations</div>
</VCol>
<VCol cols="6" md="3" class="text-center">
<div class="text-h3 font-weight-bold text-primary">24/7</div>
<div class="text-body-1 text-medium-emphasis mt-1">Support</div>
</VCol>
</VRow>
</VContainer>
</div>
</div>
</template>

View File

@@ -0,0 +1,153 @@
<script lang="ts" setup>
import { useForm } from '@inertiajs/vue3'
import MarketingLayout from '@/Layouts/MarketingLayout.vue'
import AppTextField from '@/Components/app-form-elements/AppTextField.vue'
import AppSelect from '@/Components/app-form-elements/AppSelect.vue'
import AppTextarea from '@/Components/app-form-elements/AppTextarea.vue'
defineOptions({ layout: MarketingLayout })
const form = useForm({
name: '',
email: '',
subject: '',
message: '',
})
const subjects = [
'General Inquiry',
'Sales',
'Technical Support',
'Billing',
'Partnership',
'Other',
]
function submit(): void {
form.post('/contact', {
preserveScroll: true,
onSuccess: () => form.reset(),
})
}
const contactInfo = [
{ icon: 'tabler-mail', title: 'Email', detail: 'support@ezscale.cloud', href: 'mailto:support@ezscale.cloud' },
{ icon: 'tabler-clock', title: 'Support Hours', detail: '24/7/365', href: null },
{ icon: 'tabler-message-circle', title: 'Live Chat', detail: 'Available on dashboard', href: null },
]
</script>
<template>
<div>
<VContainer class="py-16">
<div class="text-center mb-12">
<h1 class="text-h2 font-weight-bold mb-3">Contact Us</h1>
<p class="text-h6 text-medium-emphasis font-weight-regular">
Have a question? We'd love to hear from you.
</p>
</div>
<VRow>
<!-- Contact Form -->
<VCol cols="12" md="7">
<VCard variant="outlined">
<VCardText class="pa-6">
<h2 class="text-h5 font-weight-bold mb-6">Send us a message</h2>
<form @submit.prevent="submit">
<VRow>
<VCol cols="12" sm="6">
<AppTextField
v-model="form.name"
label="Name"
placeholder="John Doe"
:error-messages="form.errors.name"
required
/>
</VCol>
<VCol cols="12" sm="6">
<AppTextField
v-model="form.email"
label="Email"
type="email"
placeholder="john@example.com"
:error-messages="form.errors.email"
required
/>
</VCol>
<VCol cols="12">
<AppSelect
v-model="form.subject"
:items="subjects"
label="Subject"
:error-messages="form.errors.subject"
required
/>
</VCol>
<VCol cols="12">
<AppTextarea
v-model="form.message"
label="Message"
rows="5"
placeholder="How can we help you?"
:error-messages="form.errors.message"
required
/>
</VCol>
<VCol cols="12">
<VBtn
type="submit"
color="primary"
size="large"
:loading="form.processing"
:disabled="form.processing"
>
Send Message
<VIcon icon="tabler-send" end />
</VBtn>
</VCol>
</VRow>
</form>
</VCardText>
</VCard>
</VCol>
<!-- Contact Info -->
<VCol cols="12" md="5">
<div class="d-flex flex-column ga-4">
<VCard v-for="info in contactInfo" :key="info.title" variant="outlined">
<VCardText class="d-flex align-center ga-4">
<VAvatar color="primary" variant="tonal" size="48">
<VIcon :icon="info.icon" size="24" />
</VAvatar>
<div>
<div class="text-subtitle-2 text-medium-emphasis">{{ info.title }}</div>
<a
v-if="info.href"
:href="info.href"
class="text-body-1 font-weight-medium text-decoration-none text-primary"
>
{{ info.detail }}
</a>
<div v-else class="text-body-1 font-weight-medium">{{ info.detail }}</div>
</div>
</VCardText>
</VCard>
<VCard variant="outlined" color="primary">
<VCardText class="pa-6">
<h3 class="text-h6 font-weight-bold mb-2">Need immediate help?</h3>
<p class="text-body-2 mb-3">
Our support team is available 24/7 through your account dashboard.
</p>
<VBtn variant="outlined" size="small">
Visit Knowledge Base
</VBtn>
</VCardText>
</VCard>
</div>
</VCol>
</VRow>
</VContainer>
</div>
</template>

View File

@@ -0,0 +1,304 @@
<script lang="ts" setup>
import { usePage } from '@inertiajs/vue3'
import { computed } from 'vue'
import MarketingLayout from '@/Layouts/MarketingLayout.vue'
defineOptions({ layout: MarketingLayout })
interface PageProps {
domains: { marketing: string; account: string; admin: string }
}
const page = usePage()
const props = computed(() => page.props as unknown as PageProps)
const accountUrl = computed(() => `https://${props.value.domains?.account}`)
interface ServerConfig {
model: string
formFactor: string
cpu: string
coresThreads: string
clockSpeed: string
ram: string
bays: string
price: string
inStock: boolean
}
const features = [
{ icon: 'tabler-cpu', title: 'Dedicated Hardware', description: 'No shared resources — all CPU, RAM, and storage are exclusively yours.' },
{ icon: 'tabler-dashboard', title: 'SynergyCP Access', description: 'Full server management with SynergyCP panel including IPMI, rDNS, and OS reload.' },
{ icon: 'tabler-network', title: '1Gbps Network', description: '1 Gbps port with 10 TB bandwidth included on every server.' },
{ icon: 'tabler-lock', title: 'RAID Support', description: 'Enterprise RAID controllers available for data redundancy and performance.' },
{ icon: 'tabler-clock', title: 'Same-Day Setup', description: 'Most in-stock servers deployed same-day, subject to availability.' },
{ icon: 'tabler-headset', title: '24/7 Support', description: 'Expert engineers available around the clock for hardware and network issues.' },
]
const servers: ServerConfig[] = [
{
model: 'Dell R330 LFF',
formFactor: '4-Bay',
cpu: '1x Intel Xeon E3-1220 v5',
coresThreads: '4C/4T',
clockSpeed: '3.0/3.5 GHz',
ram: '16 GB',
bays: '4x 3.5"',
price: '$44.39',
inStock: true,
},
{
model: 'Dell R420 LFF',
formFactor: '4-Bay',
cpu: '2x Intel Xeon E5-2430v2',
coresThreads: '12C/24T',
clockSpeed: '2.5/3.0 GHz',
ram: '32 GB',
bays: '4x 3.5"',
price: '$58.79',
inStock: false,
},
{
model: 'Dell R620 SFF',
formFactor: '10-Bay',
cpu: '2x Intel Xeon E5-2667v2',
coresThreads: '16C/32T',
clockSpeed: '3.3/4.0 GHz',
ram: '32 GB',
bays: '10x 2.5"',
price: '$61.19',
inStock: false,
},
{
model: 'Dell R620 SFF',
formFactor: '8-Bay',
cpu: '2x Intel Xeon E5-2667v2',
coresThreads: '16C/32T',
clockSpeed: '3.3/4.0 GHz',
ram: '32 GB',
bays: '8x 2.5"',
price: '$61.19',
inStock: false,
},
{
model: 'Dell R520 LFF',
formFactor: '8-Bay',
cpu: '2x Intel Xeon E5-2420v2',
coresThreads: '12C/24T',
clockSpeed: '2.2/2.7 GHz',
ram: '32 GB',
bays: '8x 3.5"',
price: '$64.79',
inStock: true,
},
{
model: 'Dell R430 LFF',
formFactor: '4-Bay',
cpu: '2x Intel Xeon E5-2667v4',
coresThreads: '16C/32T',
clockSpeed: '3.2/3.6 GHz',
ram: '32 GB',
bays: '4x 3.5"',
price: '$87.59',
inStock: true,
},
{
model: 'Dell R630 SFF',
formFactor: '8-Bay',
cpu: '2x Intel Xeon E5-2697A v4',
coresThreads: '32C/64T',
clockSpeed: '2.6/3.6 GHz',
ram: '32 GB',
bays: '8x 2.5"',
price: '$93.59',
inStock: true,
},
{
model: 'Dell R730 LFF',
formFactor: '8-Bay',
cpu: '2x Intel Xeon E5-2680v4',
coresThreads: '28C/56T',
clockSpeed: '2.4/3.3 GHz',
ram: '32 GB',
bays: '8x 3.5"',
price: '$107.99',
inStock: true,
},
]
const included = [
{ icon: 'tabler-world', label: '10 TB Bandwidth' },
{ icon: 'tabler-network', label: '1 Gbps Port' },
{ icon: 'tabler-map-pin', label: 'Atlanta, GA Datacenter' },
{ icon: 'tabler-address-book', label: '1 IPv4 Address' },
{ icon: 'tabler-hexagons', label: '1x /64 IPv6 Subnet' },
{ icon: 'tabler-dashboard', label: 'SynergyCP Panel' },
]
</script>
<template>
<div>
<!-- Hero -->
<div class="py-16" style="background: linear-gradient(135deg, rgb(var(--v-theme-success), 0.1), rgb(var(--v-theme-surface)));">
<VContainer class="text-center">
<VChip color="success" variant="tonal" class="mb-4">Dedicated Servers</VChip>
<h1 class="text-h2 font-weight-bold mb-3">Bare Metal Power</h1>
<p class="text-h6 text-medium-emphasis font-weight-regular mb-8 mx-auto" style="max-width: 600px;">
Enterprise-grade Dell PowerEdge servers with full root access, SynergyCP management, and same-day deployment from our Atlanta datacenter.
</p>
<a :href="accountUrl + '/register'" class="text-decoration-none">
<VBtn color="success" size="x-large" rounded="lg">
Configure Server
<VIcon icon="tabler-arrow-right" end />
</VBtn>
</a>
</VContainer>
</div>
<!-- Features -->
<VContainer class="py-16">
<div class="text-center mb-12">
<h2 class="text-h3 font-weight-bold mb-3">Enterprise Hardware</h2>
<p class="text-body-1 text-medium-emphasis">Every dedicated server comes with these features included.</p>
</div>
<VRow>
<VCol v-for="feature in features" :key="feature.title" cols="12" sm="6" md="4">
<div class="d-flex ga-3 mb-4">
<VAvatar color="success" 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>
<!-- Server Configurations -->
<div class="bg-surface-variant py-16">
<VContainer>
<div class="text-center mb-8">
<h2 class="text-h3 font-weight-bold mb-3">Server Configurations</h2>
<p class="text-body-1 text-medium-emphasis">Real Dell PowerEdge servers. Storage sold separately -- configure your drives at checkout.</p>
</div>
<VRow>
<VCol v-for="(server, index) in servers" :key="index" cols="12" sm="6" lg="3">
<VCard
variant="outlined"
class="h-100"
:class="{ 'server-card-unavailable': !server.inStock }"
:style="server.inStock ? {} : { opacity: 0.7 }"
>
<VCardText class="pa-5">
<!-- Header with model and stock status -->
<div class="d-flex align-center justify-space-between mb-1">
<h3 class="text-subtitle-1 font-weight-bold">{{ server.model }}</h3>
<VChip
:color="server.inStock ? 'success' : 'error'"
size="small"
variant="tonal"
>
{{ server.inStock ? 'In Stock' : 'Sold Out' }}
</VChip>
</div>
<p class="text-caption text-medium-emphasis mb-3">{{ server.formFactor }}</p>
<!-- Price -->
<div class="mb-4">
<span class="text-h4 font-weight-bold" :class="server.inStock ? 'text-success' : 'text-medium-emphasis'">{{ server.price }}</span>
<span class="text-body-2 text-medium-emphasis">/mo</span>
</div>
<VDivider class="mb-4" />
<!-- Specs -->
<div class="mb-4">
<div class="d-flex align-center ga-2 py-1">
<VIcon icon="tabler-cpu" size="16" color="medium-emphasis" />
<span class="text-body-2">{{ server.cpu }}</span>
</div>
<div class="d-flex align-center ga-2 py-1">
<VIcon icon="tabler-topology-star-3" size="16" color="medium-emphasis" />
<span class="text-body-2">{{ server.coresThreads }} @ {{ server.clockSpeed }}</span>
</div>
<div class="d-flex align-center ga-2 py-1">
<VIcon icon="tabler-database" size="16" color="medium-emphasis" />
<span class="text-body-2">{{ server.ram }} RAM</span>
</div>
<div class="d-flex align-center ga-2 py-1">
<VIcon icon="tabler-server" size="16" color="medium-emphasis" />
<span class="text-body-2">{{ server.bays }} drive bays</span>
</div>
</div>
<!-- Order Button -->
<a
v-if="server.inStock"
:href="accountUrl + '/register'"
class="text-decoration-none d-block"
>
<VBtn color="success" variant="tonal" block>
Order Now
</VBtn>
</a>
<VBtn
v-else
color="default"
variant="tonal"
block
disabled
>
Unavailable
</VBtn>
</VCardText>
</VCard>
</VCol>
</VRow>
</VContainer>
</div>
<!-- Included With Every Server -->
<VContainer class="py-16">
<div class="text-center mb-8">
<h2 class="text-h3 font-weight-bold mb-3">Included With Every Server</h2>
<p class="text-body-1 text-medium-emphasis">No hidden fees. All servers come with these essentials.</p>
</div>
<VRow justify="center">
<VCol v-for="item in included" :key="item.label" cols="6" sm="4" md="2">
<div class="text-center">
<VAvatar color="success" variant="tonal" size="48" class="mb-3">
<VIcon :icon="item.icon" size="24" />
</VAvatar>
<p class="text-body-2 font-weight-medium mb-0">{{ item.label }}</p>
</div>
</VCol>
</VRow>
</VContainer>
<!-- CTA -->
<div class="py-12" style="background: linear-gradient(135deg, rgb(var(--v-theme-success), 0.08), rgb(var(--v-theme-surface)));">
<VContainer class="text-center">
<h2 class="text-h4 font-weight-bold mb-3">Need a Custom Configuration?</h2>
<p class="text-body-1 text-medium-emphasis mb-6">
Contact us for custom builds, bulk orders, or servers with specific hardware requirements.
</p>
<div class="d-flex ga-3 justify-center flex-wrap">
<a :href="accountUrl + '/register'" class="text-decoration-none">
<VBtn color="success" size="large" rounded="lg">
Get Started
<VIcon icon="tabler-arrow-right" end />
</VBtn>
</a>
<a href="/contact" class="text-decoration-none">
<VBtn color="success" variant="outlined" size="large" rounded="lg">
Contact Sales
</VBtn>
</a>
</div>
</VContainer>
</div>
</div>
</template>

View File

@@ -0,0 +1,97 @@
<script lang="ts" setup>
import { usePage } from '@inertiajs/vue3'
import { computed } from 'vue'
import MarketingLayout from '@/Layouts/MarketingLayout.vue'
defineOptions({ layout: MarketingLayout })
interface PageProps {
domains: { marketing: string; account: string; admin: string }
}
const page = usePage()
const props = computed(() => page.props as unknown as PageProps)
const accountUrl = computed(() => `https://${props.value.domains?.account}`)
const games = [
{ name: 'Minecraft', icon: 'tabler-cube', description: 'Java & Bedrock editions with mod support.', startingAt: '$7.99/mo' },
{ name: 'Rust', icon: 'tabler-shield', description: 'High-performance Rust servers with Oxide support.', startingAt: '$14.99/mo' },
{ name: 'ARK: Survival Evolved', icon: 'tabler-dinosaur', description: 'ARK servers with cluster support.', startingAt: '$19.99/mo' },
{ name: 'Valheim', icon: 'tabler-sword', description: 'Dedicated Valheim servers with mod support.', startingAt: '$9.99/mo' },
{ name: 'CS2', icon: 'tabler-crosshair', description: 'Counter-Strike 2 competitive and casual servers.', startingAt: '$12.99/mo' },
{ name: 'Palworld', icon: 'tabler-paw', description: 'Palworld dedicated servers with full configuration.', startingAt: '$14.99/mo' },
]
const features = [
{ icon: 'tabler-bolt', title: 'Low Latency', description: 'Optimized network routing for minimal ping.' },
{ icon: 'tabler-puzzle', title: 'Mod Support', description: 'Easy plugin and mod installation with one-click tools.' },
{ icon: 'tabler-shield-check', title: 'DDoS Protection', description: 'Always-on protection to keep your server online.' },
{ icon: 'tabler-clock', title: 'Instant Setup', description: 'Your game server is ready in under 5 minutes.' },
]
</script>
<template>
<div>
<!-- Hero -->
<div class="py-16" style="background: linear-gradient(135deg, rgb(var(--v-theme-error), 0.1), rgb(var(--v-theme-surface)));">
<VContainer class="text-center">
<VChip color="error" variant="tonal" class="mb-4">Game Servers</VChip>
<h1 class="text-h2 font-weight-bold mb-3">Game Server Hosting</h1>
<p class="text-h6 text-medium-emphasis font-weight-regular mb-8 mx-auto" style="max-width: 600px;">
Low-latency game server hosting with instant setup, mod support, and DDoS protection.
</p>
<a :href="accountUrl + '/register'" class="text-decoration-none">
<VBtn color="error" size="x-large" rounded="lg">
Get Your Server
<VIcon icon="tabler-arrow-right" end />
</VBtn>
</a>
</VContainer>
</div>
<!-- Supported Games -->
<VContainer class="py-16">
<div class="text-center mb-12">
<h2 class="text-h3 font-weight-bold mb-3">Supported Games</h2>
<p class="text-body-1 text-medium-emphasis">Popular titles with more being added regularly.</p>
</div>
<VRow>
<VCol v-for="game in games" :key="game.name" cols="12" sm="6" md="4">
<VCard variant="outlined" class="h-100">
<VCardText class="d-flex align-center ga-4">
<VAvatar color="error" variant="tonal" size="48">
<VIcon :icon="game.icon" size="24" />
</VAvatar>
<div>
<h3 class="text-subtitle-1 font-weight-bold">{{ game.name }}</h3>
<p class="text-body-2 text-medium-emphasis mb-1">{{ game.description }}</p>
<span class="text-body-2 text-error font-weight-medium">From {{ game.startingAt }}</span>
</div>
</VCardText>
</VCard>
</VCol>
</VRow>
</VContainer>
<!-- Features -->
<div class="bg-surface-variant py-16">
<VContainer>
<div class="text-center mb-12">
<h2 class="text-h3 font-weight-bold mb-3">Why Gamers Choose EZSCALE</h2>
</div>
<VRow>
<VCol v-for="feature in features" :key="feature.title" cols="12" sm="6" md="3">
<VCard variant="flat" class="text-center pa-4 h-100 bg-transparent">
<VAvatar color="error" variant="tonal" size="56" class="mb-3">
<VIcon :icon="feature.icon" size="28" />
</VAvatar>
<h3 class="text-subtitle-1 font-weight-bold mb-1">{{ feature.title }}</h3>
<p class="text-body-2 text-medium-emphasis mb-0">{{ feature.description }}</p>
</VCard>
</VCol>
</VRow>
</VContainer>
</div>
</div>
</template>

View File

@@ -0,0 +1,113 @@
<script lang="ts" setup>
import { usePage } from '@inertiajs/vue3'
import { computed } from 'vue'
import MarketingLayout from '@/Layouts/MarketingLayout.vue'
defineOptions({ layout: MarketingLayout })
interface PageProps {
domains: { marketing: string; account: string; admin: string }
}
const page = usePage()
const props = computed(() => page.props as unknown as PageProps)
const accountUrl = computed(() => `https://${props.value.domains?.account}`)
const features = [
{ icon: 'tabler-server', title: 'VPS Hosting', description: 'SSD VPS with VirtFusion panel, instant provisioning, and full root access. Starting at $4.20/mo.', href: '/vps-hosting', color: 'primary' },
{ icon: 'tabler-server-2', title: 'Dedicated Servers', description: 'Dell PowerEdge servers with SynergyCP management and 1Gbps connectivity. Starting at $44.39/mo.', href: '/dedicated-servers', color: 'success' },
{ icon: 'tabler-world', title: 'Web Hosting', description: 'Web hosting with Enhance panel, free SSL, Cloudflare DNS, and Redis cache. Starting at $2.39/mo.', href: '/web-hosting', color: 'warning' },
{ icon: 'tabler-device-gamepad-2', title: 'Game Servers', description: 'Low-latency game hosting for Minecraft, Rust, ARK, and more. Coming soon.', href: '/game-servers', color: 'error' },
]
const stats = [
{ value: '10,000+', label: 'Active Customers' },
{ value: '99.99%', label: 'Uptime SLA' },
{ value: '50+', label: 'Server Locations' },
{ value: '24/7', label: 'Expert Support' },
]
</script>
<template>
<div>
<!-- Hero Section -->
<div class="py-16" style="background: linear-gradient(135deg, rgb(var(--v-theme-primary), 0.1), rgb(var(--v-theme-surface)));">
<VContainer>
<VRow align="center" justify="center">
<VCol cols="12" md="8" class="text-center">
<h1 class="text-h2 text-md-h1 font-weight-bold mb-4">
Cloud Hosting
<span class="text-primary">Made Simple</span>
</h1>
<p class="text-h6 text-medium-emphasis font-weight-regular mb-8 mx-auto" style="max-width: 600px;">
VPS, Dedicated Servers, Web Hosting, and Game Servers. Deploy in minutes with enterprise-grade infrastructure.
</p>
<div class="d-flex justify-center ga-4 flex-wrap">
<a :href="accountUrl + '/register'" class="text-decoration-none">
<VBtn color="primary" size="x-large" rounded="lg">
Start Free Trial
<VIcon icon="tabler-arrow-right" end />
</VBtn>
</a>
<a href="/pricing" class="text-decoration-none">
<VBtn variant="outlined" size="x-large" rounded="lg">
View Pricing
</VBtn>
</a>
</div>
</VCol>
</VRow>
</VContainer>
</div>
<!-- Features Section -->
<VContainer class="py-16">
<div class="text-center mb-12">
<h2 class="text-h3 font-weight-bold mb-3">Our Products</h2>
<p class="text-body-1 text-medium-emphasis">Everything you need to build, deploy, and scale.</p>
</div>
<VRow>
<VCol v-for="feature in features" :key="feature.title" cols="12" sm="6" md="3">
<VCard variant="outlined" class="h-100" :href="feature.href">
<VCardText class="text-center pa-6">
<VAvatar :color="feature.color" variant="tonal" size="64" class="mb-4">
<VIcon :icon="feature.icon" size="32" />
</VAvatar>
<h3 class="text-h6 font-weight-bold mb-2">{{ feature.title }}</h3>
<p class="text-body-2 text-medium-emphasis mb-0">{{ feature.description }}</p>
</VCardText>
</VCard>
</VCol>
</VRow>
</VContainer>
<!-- Stats Section -->
<div class="bg-surface-variant py-16">
<VContainer>
<VRow>
<VCol v-for="stat in stats" :key="stat.label" cols="6" md="3" class="text-center">
<div class="text-h3 font-weight-bold text-primary">{{ stat.value }}</div>
<div class="text-body-1 text-medium-emphasis mt-1">{{ stat.label }}</div>
</VCol>
</VRow>
</VContainer>
</div>
<!-- CTA Section -->
<VContainer class="py-16">
<VCard color="primary" class="text-center pa-12">
<h2 class="text-h3 font-weight-bold text-white mb-3">Ready to get started?</h2>
<p class="text-h6 font-weight-regular mb-6" style="opacity: 0.9;">
Deploy your first server in under 60 seconds.
</p>
<a :href="accountUrl + '/register'" class="text-decoration-none">
<VBtn color="white" size="x-large" rounded="lg">
Create Free Account
<VIcon icon="tabler-arrow-right" end />
</VBtn>
</a>
</VCard>
</VContainer>
</div>
</template>

View File

@@ -0,0 +1,381 @@
<script lang="ts" setup>
import { usePage } from '@inertiajs/vue3'
import { computed } from 'vue'
import MarketingLayout from '@/Layouts/MarketingLayout.vue'
defineOptions({ layout: MarketingLayout })
interface Plan {
id: number
name: string
slug: string
description: string | null
service_type: string
price: string
currency: string
billing_cycle: string
features: Record<string, string> | null
stock_quantity: number | null
status: string
sort_order: number
}
interface PageProps {
plans: Plan[]
domains: { marketing: string; account: string; admin: string }
}
const page = usePage()
const props = computed(() => page.props as unknown as PageProps)
const accountUrl = computed(() => `https://${props.value.domains?.account}`)
const plans = computed(() => props.value.plans || [])
function getMonthlyPrice(plan: Plan): string {
const price = parseFloat(plan.price ?? '0') || 0
return price % 1 === 0 ? price.toString() : price.toFixed(2)
}
function getPlanColor(index: number): string {
const colors = ['primary', 'success', 'warning', 'error', 'info']
return colors[index % colors.length]
}
function isPopular(index: number): boolean {
return plans.value.length > 1 && index === 1
}
// Feature comparison data
const featureComparison = computed(() => {
if (plans.value.length === 0) return []
const allFeatures = new Set<string>()
plans.value.forEach(plan => {
if (plan.features) {
Object.keys(plan.features).forEach(f => allFeatures.add(f))
}
})
return Array.from(allFeatures).map(feature => ({
feature,
plans: plans.value.map(plan => ({
value: plan.features?.[feature] ?? null,
})),
}))
})
const faqs = [
{
question: 'Can I upgrade my plan later?',
answer: 'Yes! You can upgrade or downgrade your plan at any time from your account dashboard. Changes take effect immediately and billing is prorated.',
},
{
question: 'What payment methods do you accept?',
answer: 'We accept all major credit cards (Visa, Mastercard, American Express) via Stripe, as well as PayPal. Your payment information is always kept safe and secure.',
},
{
question: 'Is there a money-back guarantee?',
answer: 'Yes, all plans come with a 30-day money-back guarantee. If you\'re not satisfied, contact support for a full refund.',
},
{
question: 'Do you offer custom configurations?',
answer: 'Absolutely. Contact our sales team for custom server configurations, bulk pricing, or enterprise solutions tailored to your needs.',
},
]
</script>
<template>
<div class="pricing-page">
<VCard class="pricing-card" flat>
<!-- Plan Cards Section -->
<VContainer>
<div class="text-center">
<h3 class="text-h3 pricing-title mb-2">
Pricing Plans
</h3>
<p class="mb-0 text-body-1">
All plans include 24/7 monitoring and enterprise-grade infrastructure.
</p>
<p class="mb-2 text-body-1">
Choose the best plan to fit your needs.
</p>
</div>
<!-- Plan Cards -->
<VRow v-if="plans.length">
<VCol
v-for="(plan, index) in plans"
:key="plan.id"
cols="12"
md="4"
>
<VCard
flat
border
:class="isPopular(index) ? 'border-primary border-opacity-100' : ''"
>
<VCardText
style="block-size: 3.75rem;"
class="text-end"
>
<VChip
v-show="isPopular(index)"
label
color="primary"
size="small"
>
Popular
</VChip>
</VCardText>
<VCardText>
<!-- Plan Icon -->
<div class="text-center mb-5">
<VAvatar
:color="getPlanColor(index)"
variant="tonal"
size="80"
>
<VIcon
:icon="plan.service_type === 'vps' ? 'tabler-cloud' : plan.service_type === 'dedicated' ? 'tabler-server' : plan.service_type === 'web' ? 'tabler-world' : plan.service_type === 'game' ? 'tabler-device-gamepad-2' : 'tabler-package'"
size="40"
/>
</VAvatar>
</div>
<!-- Plan Name -->
<h4 class="text-h4 mb-1 text-center">
{{ plan.name }}
</h4>
<p class="mb-0 text-body-1 text-center">
{{ plan.description || 'High performance hosting' }}
</p>
<!-- Plan Price -->
<div class="position-relative">
<div class="d-flex justify-center pt-5 pb-10">
<div class="text-body-1 align-self-start font-weight-medium">
$
</div>
<h1 class="text-h1 font-weight-medium text-primary">
{{ getMonthlyPrice(plan) }}
</h1>
<div class="text-body-1 font-weight-medium align-self-end">
/month
</div>
</div>
</div>
<!-- Plan Features -->
<VList class="card-list mb-4">
<VListItem
v-for="(value, feature) in plan.features"
:key="String(feature)"
>
<template #prepend>
<VIcon
size="8"
icon="tabler-circle-filled"
color="rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity))"
/>
</template>
<VListItemTitle class="text-body-1">
{{ value }}
</VListItemTitle>
</VListItem>
</VList>
<!-- Plan CTA -->
<a
:href="accountUrl + '/register'"
class="text-decoration-none d-block"
>
<VBtn
block
:variant="isPopular(index) ? 'elevated' : 'tonal'"
:active="false"
>
Choose Plan
</VBtn>
</a>
</VCardText>
</VCard>
</VCol>
</VRow>
<VCard v-else variant="outlined" class="pa-12 text-center">
<VIcon icon="tabler-package" size="48" class="text-medium-emphasis mb-4" />
<h3 class="text-h5 font-weight-bold mb-2">Plans Coming Soon</h3>
<p class="text-body-1 text-medium-emphasis">
We're finalizing our plans. Check back soon or sign up to be notified.
</p>
</VCard>
</VContainer>
<!-- Feature Comparison Table -->
<VContainer v-if="plans.length && featureComparison.length">
<VCardText class="text-center py-16 pricing-section">
<h3 class="text-h3 mb-2">
Pick a plan that works best for you
</h3>
<p class="text-body-1">
Stay cool, we have a 30-day money back guarantee!
</p>
<VTable class="text-no-wrap border rounded pricing-table">
<thead>
<tr>
<th scope="col" class="py-4">
<div>Features</div>
<div class="text-body-2">Plan Comparison</div>
</th>
<th
v-for="(plan, index) in plans"
:key="plan.id"
scope="col"
class="text-center py-4"
>
<div class="position-relative">
{{ plan.name }}
<VAvatar
v-if="isPopular(index)"
size="20"
class="ms-2 position-absolute"
variant="elevated"
color="primary"
style="inset-block-end: 7px;"
>
<VIcon
icon="tabler-star"
size="14"
color="white"
/>
</VAvatar>
</div>
<div class="text-body-2">
${{ getMonthlyPrice(plan) }}/Month
</div>
</th>
</tr>
</thead>
<tbody>
<tr
v-for="row in featureComparison"
:key="row.feature"
>
<td class="text-start text-body-1 text-high-emphasis">
{{ row.feature }}
</td>
<td
v-for="(planData, pIndex) in row.plans"
:key="pIndex"
class="text-center"
>
<span v-if="planData.value" class="text-body-1">
{{ planData.value }}
</span>
<VIcon
v-else
icon="tabler-minus"
size="14"
color="secondary"
/>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td class="py-2" />
<td
v-for="(plan, index) in plans"
:key="plan.id"
class="text-center py-2"
>
<a
:href="accountUrl + '/register'"
class="text-decoration-none"
>
<VBtn
:variant="isPopular(index) ? 'elevated' : 'tonal'"
>
Choose Plan
</VBtn>
</a>
</td>
</tr>
</tfoot>
</VTable>
</VCardText>
</VContainer>
<!-- FAQ Section -->
<div class="faq-section-bg">
<VContainer>
<VCardText class="py-10 py-sm-16 pricing-section">
<div class="text-center">
<h4 class="text-h4 mb-2">
FAQ's
</h4>
<p class="text-body-1 mb-6">
Let us help answer the most common questions.
</p>
</div>
<VRow justify="center">
<VCol cols="12" md="8">
<VExpansionPanels>
<VExpansionPanel
v-for="(faq, index) in faqs"
:key="faq.question"
:title="faq.question"
:text="faq.answer"
:value="index"
/>
</VExpansionPanels>
</VCol>
</VRow>
</VCardText>
</VContainer>
</div>
</VCard>
</div>
</template>
<style lang="scss" scoped>
.pricing-card {
padding-block-start: 5rem !important;
}
.pricing-title {
font-weight: 800;
}
.card-list {
--v-card-list-gap: 1rem;
}
.pricing-section {
padding-block: 5.25rem !important;
padding-inline: 0 !important;
}
.faq-section-bg {
background-color: rgba(var(--v-theme-on-surface), var(--v-hover-opacity));
}
.pricing-table {
tr:nth-child(even) {
background: rgba(var(--v-theme-on-surface), var(--v-hover-opacity));
}
}
</style>
<style lang="scss">
.pricing-page {
@media (min-width: 600px) and (max-width: 960px) {
.v-container {
padding-inline: 2rem !important;
}
}
}
</style>

View File

@@ -0,0 +1,102 @@
<script lang="ts" setup>
import MarketingLayout from '@/Layouts/MarketingLayout.vue'
defineOptions({ layout: MarketingLayout })
const products = [
{
icon: 'tabler-server',
title: 'VPS Hosting',
description: 'High-performance SSD VPS with full root access, VirtFusion panel, and instant provisioning.',
features: ['SSD Storage', 'Full Root Access', 'Instant Provisioning', 'DDoS Protection'],
href: '/vps-hosting',
color: 'primary',
startingAt: '$4.20/mo',
},
{
icon: 'tabler-server-2',
title: 'Dedicated Servers',
description: 'Enterprise-grade Dell servers with dedicated hardware, SynergyCP management, and 1Gbps connectivity.',
features: ['Dedicated Hardware', 'SynergyCP Panel', '1Gbps Network', 'RAID Support'],
href: '/dedicated-servers',
color: 'success',
startingAt: '$44.39/mo',
},
{
icon: 'tabler-world',
title: 'Web Hosting',
description: 'Reliable web hosting with Enhance panel, free SSL, Cloudflare DNS, and Redis cache.',
features: ['Enhance Panel', 'Free SSL', 'Cloudflare DNS', 'Redis Cache'],
href: '/web-hosting',
color: 'warning',
startingAt: '$2.39/mo',
},
{
icon: 'tabler-device-gamepad-2',
title: 'Game Servers',
description: 'Contact us for custom game server hosting. Coming soon with low-latency optimized infrastructure.',
features: ['Low Latency Network', 'Mod Support', 'Instant Setup', 'DDoS Protection'],
href: '/game-servers',
color: 'error',
startingAt: 'Coming Soon',
},
{
icon: 'tabler-database',
title: 'MySQL Hosting',
description: 'Managed MySQL databases with daily backups and SSL encryption.',
features: ['Managed MySQL', 'Daily Backups', 'SSL Encrypted', 'High Availability'],
href: '/mysql-hosting',
color: 'info',
startingAt: '$6.00/mo',
},
]
</script>
<template>
<div>
<VContainer class="py-16">
<div class="text-center mb-12">
<h1 class="text-h2 font-weight-bold mb-3">Our Products</h1>
<p class="text-h6 text-medium-emphasis font-weight-regular">
Enterprise-grade hosting solutions for every need.
</p>
</div>
<VRow>
<VCol v-for="product in products" :key="product.title" cols="12" md="6">
<VCard variant="outlined" class="h-100">
<VCardText class="pa-6">
<div class="d-flex align-center ga-4 mb-4">
<VAvatar :color="product.color" variant="tonal" size="56">
<VIcon :icon="product.icon" size="28" />
</VAvatar>
<div>
<h2 class="text-h5 font-weight-bold">{{ product.title }}</h2>
<span class="text-body-2 text-medium-emphasis">Starting at {{ product.startingAt }}</span>
</div>
</div>
<p class="text-body-1 text-medium-emphasis mb-4">{{ product.description }}</p>
<VList density="compact" class="pa-0 mb-4">
<VListItem v-for="feature in product.features" :key="feature" class="px-0">
<template #prepend>
<VIcon icon="tabler-check" :color="product.color" size="18" class="me-2" />
</template>
<VListItemTitle class="text-body-2">{{ feature }}</VListItemTitle>
</VListItem>
</VList>
<a :href="product.href" class="text-decoration-none">
<VBtn :color="product.color" variant="tonal" block>
Learn More
<VIcon icon="tabler-arrow-right" end />
</VBtn>
</a>
</VCardText>
</VCard>
</VCol>
</VRow>
</VContainer>
</div>
</template>

View File

@@ -0,0 +1,169 @@
<script lang="ts" setup>
import { usePage } from '@inertiajs/vue3'
import { computed } from 'vue'
import MarketingLayout from '@/Layouts/MarketingLayout.vue'
defineOptions({ layout: MarketingLayout })
interface PageProps {
domains: { marketing: string; account: string; admin: string }
}
interface VpsPlan {
name: string
cpu: string
ram: string
storage: string
bandwidth: string
price: string
}
interface Feature {
icon: string
title: string
description: string
}
const page = usePage()
const props = computed(() => page.props as unknown as PageProps)
const accountUrl = computed<string>(() => `https://${props.value.domains?.account}`)
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-shield-check', title: 'DDoS Protection', description: 'Enterprise-grade protection against volumetric attacks.' },
{ icon: 'tabler-rocket', title: 'Instant Provisioning', description: 'Your server is deployed within seconds of ordering.' },
{ icon: 'tabler-refresh', title: 'VM Backups', description: 'Built-in VM backup and snapshot functionality.' },
{ icon: 'tabler-terminal', title: 'Full Root Access', description: 'Complete control over your server environment.' },
{ icon: 'tabler-server', title: 'VirtFusion Panel', description: 'Powerful control panel for managing your VPS with ease.' },
]
const plans: VpsPlan[] = [
{ name: 'Micro VPS', cpu: '1 vCPU', ram: '1 GB', storage: '25 GB SSD', bandwidth: '2 TB', price: '$4.20' },
{ name: 'Mini VPS', cpu: '1 vCPU', ram: '2 GB', storage: '50 GB SSD', bandwidth: '4 TB', price: '$6.00' },
{ name: 'Dev Starter', cpu: '2 vCPU', ram: '2 GB', storage: '60 GB SSD', bandwidth: '4 TB', price: '$8.00' },
{ name: 'Basic VPS', cpu: '2 vCPU', ram: '4 GB', storage: '80 GB SSD', bandwidth: '6 TB', price: '$12.00' },
{ name: 'Storage Box', cpu: '2 vCPU', ram: '2 GB', storage: '500 GB SSD', bandwidth: '8 TB', price: '$15.00' },
{ name: 'Standard VPS', cpu: '4 vCPU', ram: '8 GB', storage: '160 GB SSD', bandwidth: '8 TB', price: '$15.60' },
{ name: 'RAM Optimized', cpu: '4 vCPU', ram: '16 GB', storage: '240 GB SSD', bandwidth: '10 TB', price: '$19.00' },
{ name: 'Advanced VPS', cpu: '6 vCPU', ram: '16 GB', storage: '320 GB SSD', bandwidth: '10 TB', price: '$21.60' },
{ name: 'Pro VPS', cpu: '8 vCPU', ram: '32 GB', storage: '640 GB SSD', bandwidth: '16 TB', price: '$30.00' },
]
const includedFeatures: string[] = [
'1 IPv4 & 1 /64 IPv6',
'Near instant provisioning',
'VM backups',
'Windows (BYOL) & Linux support',
'Intel E5-2680 v4 processors',
'Full root access',
'VirtFusion control panel',
'RAID 10 backed storage',
'14-day money back guarantee',
]
</script>
<template>
<div>
<!-- Hero -->
<div class="py-16" style="background: linear-gradient(135deg, rgb(var(--v-theme-primary), 0.1), rgb(var(--v-theme-surface)));">
<VContainer class="text-center">
<VChip color="primary" variant="tonal" class="mb-4">VPS Hosting</VChip>
<h1 class="text-h2 font-weight-bold mb-3">Virtual Private Servers</h1>
<p class="text-h6 text-medium-emphasis font-weight-regular mb-4 mx-auto" 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">$4.20/mo</span>
</p>
<a :href="accountUrl + '/register'" class="text-decoration-none">
<VBtn color="primary" size="x-large" rounded="lg">
Get Started
<VIcon icon="tabler-arrow-right" end />
</VBtn>
</a>
</VContainer>
</div>
<!-- Features -->
<VContainer class="py-16">
<div class="text-center mb-12">
<h2 class="text-h3 font-weight-bold mb-3">Why Choose EZSCALE VPS?</h2>
</div>
<VRow>
<VCol v-for="feature in features" :key="feature.title" cols="12" sm="6" md="4">
<div class="d-flex ga-3 mb-4">
<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="bg-surface-variant py-16">
<VContainer>
<div class="text-center mb-8">
<h2 class="text-h3 font-weight-bold mb-3">VPS Plans</h2>
<p class="text-body-1 text-medium-emphasis">
All plans hosted in our Atlanta, GA datacenter. DDoS protection, full root access, and VirtFusion panel included.
</p>
</div>
<VCard>
<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.name">
<td class="font-weight-bold">{{ plan.name }}</td>
<td>{{ plan.cpu }}</td>
<td>{{ plan.ram }}</td>
<td>{{ plan.storage }}</td>
<td>{{ plan.bandwidth }}</td>
<td class="text-primary font-weight-bold">{{ plan.price }}/mo</td>
<td>
<a :href="accountUrl + '/register'" class="text-decoration-none">
<VBtn color="primary" size="small" variant="tonal">Order Now</VBtn>
</a>
</td>
</tr>
</tbody>
</VTable>
</VCard>
<!-- Included with all plans -->
<VCard class="mt-8 pa-6">
<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"
cols="12"
sm="6"
md="4"
>
<div class="d-flex align-center ga-2 mb-2">
<VIcon icon="tabler-circle-check" color="success" size="20" />
<span class="text-body-1">{{ item }}</span>
</div>
</VCol>
</VRow>
</VCard>
</VContainer>
</div>
</div>
</template>

View File

@@ -0,0 +1,253 @@
<script lang="ts" setup>
import { usePage } from '@inertiajs/vue3'
import { computed } from 'vue'
import MarketingLayout from '@/Layouts/MarketingLayout.vue'
defineOptions({ layout: MarketingLayout })
interface PageProps {
domains: { marketing: string; account: string; admin: string }
}
const page = usePage()
const props = computed(() => page.props as unknown as PageProps)
const accountUrl = computed(() => `https://${props.value.domains?.account}`)
interface Feature {
icon: string
title: string
description: string
}
interface Plan {
name: string
storage: string
databases: string
email: string
domains: string
bandwidth: string
ram: string
cores: string
price: string
popular?: boolean
}
const features: Feature[] = [
{ icon: 'tabler-layout-dashboard', title: 'Enhance Control Panel', description: 'Modern, intuitive control panel for effortless website and server management.' },
{ icon: 'tabler-lock', title: 'Free SSL Certificates', description: 'Auto-provisioned SSL certificates for all your domains at no extra cost.' },
{ icon: 'tabler-cloud', title: 'Cloudflare DNS', description: 'Enterprise-grade DNS with DDoS protection and global CDN included.' },
{ icon: 'tabler-database', title: 'Redis Cache', description: 'Built-in Redis object caching for blazing-fast page load times.' },
{ icon: 'tabler-brand-wordpress', title: 'WordPress Installer', description: 'One-click WordPress installation with automatic updates and optimization.' },
{ icon: 'tabler-terminal-2', title: 'SSH Access', description: 'Full SSH access for advanced users who need command-line control.' },
]
const plans: Plan[] = [
{
name: 'Small',
storage: '10 GB SSD',
databases: '2 MySQL DBs',
email: '5 Email Accounts',
domains: '1 Domain',
bandwidth: '1 TB BW',
ram: '512 MB RAM',
cores: '1 Core',
price: '$2.39',
},
{
name: 'Medium',
storage: '25 GB SSD',
databases: '6 MySQL DBs',
email: '20 Email Accounts',
domains: '4 Domains',
bandwidth: '1 TB BW',
ram: '1 GB RAM',
cores: '1 Core',
price: '$3.99',
popular: true,
},
{
name: 'Large',
storage: '100 GB SSD',
databases: 'Unlimited MySQL DBs',
email: 'Unlimited Email',
domains: '30 Domains',
bandwidth: '2 TB BW',
ram: '4 GB RAM',
cores: '4 Cores',
price: '$7.19',
},
{
name: 'Dedicated',
storage: '160 GB SSD',
databases: 'Unlimited MySQL DBs',
email: 'Unlimited Email',
domains: '100 Domains',
bandwidth: '4 TB BW',
ram: '8 GB RAM',
cores: '4 Cores',
price: '$15.99',
},
]
const includedFeatures: string[] = [
'Free SSL',
'Cloudflare DNS',
'Redis Cache',
'WordPress Installer',
'SSH Access',
'Enhance Panel',
]
</script>
<template>
<div>
<!-- Hero -->
<div class="py-16" style="background: linear-gradient(135deg, rgb(var(--v-theme-warning), 0.1), rgb(var(--v-theme-surface)));">
<VContainer class="text-center">
<VChip color="warning" variant="tonal" class="mb-4">Web Hosting</VChip>
<h1 class="text-h2 font-weight-bold mb-3">Managed Web Hosting</h1>
<p class="text-h6 text-medium-emphasis font-weight-regular mb-8 mx-auto" style="max-width: 600px;">
Fast, secure, and reliable web hosting powered by Enhance with free SSL, Cloudflare DNS, and Redis caching.
</p>
<a :href="accountUrl + '/register'" class="text-decoration-none">
<VBtn color="warning" size="x-large" rounded="lg">
Get Started
<VIcon icon="tabler-arrow-right" end />
</VBtn>
</a>
</VContainer>
</div>
<!-- Features -->
<VContainer class="py-16">
<div class="text-center mb-12">
<h2 class="text-h3 font-weight-bold mb-3">Everything You Need</h2>
<p class="text-body-1 text-medium-emphasis mx-auto" style="max-width: 550px;">
Every plan comes loaded with the tools and features you need to build and grow your website.
</p>
</div>
<VRow>
<VCol v-for="feature in features" :key="feature.title" cols="12" sm="6" md="4">
<div class="d-flex ga-3 mb-4">
<VAvatar color="warning" 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 -->
<div class="bg-surface-variant py-16">
<VContainer>
<div class="text-center mb-8">
<h2 class="text-h3 font-weight-bold mb-3">Hosting Plans</h2>
<p class="text-body-1 text-medium-emphasis mx-auto" style="max-width: 550px;">
Choose the plan that fits your needs. All plans include free SSL, Cloudflare DNS, and the Enhance control panel.
</p>
</div>
<VRow justify="center">
<VCol v-for="plan in plans" :key="plan.name" cols="12" sm="6" lg="3">
<VCard
:variant="plan.popular ? 'elevated' : 'outlined'"
:class="['h-100', { 'border-warning border-opacity-100': plan.popular }]"
:elevation="plan.popular ? 8 : 0"
>
<VCardText class="pa-6 text-center">
<VChip
v-if="plan.popular"
color="warning"
size="small"
class="mb-2"
>
Most Popular
</VChip>
<h3 class="text-h5 font-weight-bold mb-1">{{ plan.name }}</h3>
<div class="text-h4 font-weight-bold text-warning mb-4">
{{ plan.price }}<span class="text-body-2 text-medium-emphasis">/mo</span>
</div>
<VDivider class="mb-4" />
<VList density="compact" class="pa-0">
<VListItem class="px-0">
<template #prepend>
<VIcon icon="tabler-database" color="warning" size="18" class="me-2" />
</template>
<VListItemTitle class="text-body-2">{{ plan.storage }}</VListItemTitle>
</VListItem>
<VListItem class="px-0">
<template #prepend>
<VIcon icon="tabler-stack-2" color="warning" size="18" class="me-2" />
</template>
<VListItemTitle class="text-body-2">{{ plan.databases }}</VListItemTitle>
</VListItem>
<VListItem class="px-0">
<template #prepend>
<VIcon icon="tabler-mail" color="warning" size="18" class="me-2" />
</template>
<VListItemTitle class="text-body-2">{{ plan.email }}</VListItemTitle>
</VListItem>
<VListItem class="px-0">
<template #prepend>
<VIcon icon="tabler-world" color="warning" size="18" class="me-2" />
</template>
<VListItemTitle class="text-body-2">{{ plan.domains }}</VListItemTitle>
</VListItem>
<VListItem class="px-0">
<template #prepend>
<VIcon icon="tabler-arrows-transfer-up" color="warning" size="18" class="me-2" />
</template>
<VListItemTitle class="text-body-2">{{ plan.bandwidth }}</VListItemTitle>
</VListItem>
<VListItem class="px-0">
<template #prepend>
<VIcon icon="tabler-device-desktop-analytics" color="warning" size="18" class="me-2" />
</template>
<VListItemTitle class="text-body-2">{{ plan.ram }}</VListItemTitle>
</VListItem>
<VListItem class="px-0">
<template #prepend>
<VIcon icon="tabler-cpu" color="warning" size="18" class="me-2" />
</template>
<VListItemTitle class="text-body-2">{{ plan.cores }}</VListItemTitle>
</VListItem>
</VList>
<VDivider class="my-4" />
<div class="d-flex flex-wrap justify-center ga-1 mb-4">
<VChip
v-for="feat in includedFeatures"
:key="feat"
size="x-small"
variant="tonal"
color="warning"
>
{{ feat }}
</VChip>
</div>
<a :href="accountUrl + '/register'" class="text-decoration-none d-block">
<VBtn
:color="plan.popular ? 'warning' : 'warning'"
:variant="plan.popular ? 'elevated' : 'tonal'"
block
>
Choose Plan
</VBtn>
</a>
</VCardText>
</VCard>
</VCol>
</VRow>
</VContainer>
</div>
</div>
</template>

View File

@@ -0,0 +1,92 @@
<script lang="ts" setup>
import { Link } from '@inertiajs/vue3'
import AccountLayout from '@/Layouts/AccountLayout.vue'
import { formatPrice } from '@/utils/resolvers'
import type { Plan } from '@/types'
interface Props {
plansByType: Record<string, Plan[]>
}
defineOptions({ layout: AccountLayout })
defineProps<Props>()
const serviceTypeLabels: Record<string, string> = {
vps: 'VPS Servers',
dedicated: 'Dedicated Servers',
hosting: 'Web Hosting',
game: 'Game Servers',
}
</script>
<template>
<div>
<div class="text-h4 font-weight-bold mb-6">Plans &amp; Pricing</div>
<div v-for="(plans, type) in plansByType" :key="type" class="mb-10">
<div class="text-h5 font-weight-medium mb-4">
{{ serviceTypeLabels[type as string] || type }}
</div>
<VRow>
<VCol
v-for="plan in plans"
:key="plan.id"
cols="12"
md="6"
lg="4"
>
<VCard class="d-flex flex-column h-100">
<VCardTitle>{{ plan.name }}</VCardTitle>
<VCardText v-if="plan.description" class="text-medium-emphasis">
{{ plan.description }}
</VCardText>
<VCardText>
<div class="text-h4 font-weight-bold">
{{ formatPrice(plan.price, plan.billing_cycle) }}
</div>
</VCardText>
<VCardText v-if="plan.features" class="flex-grow-1">
<VList density="compact" class="pa-0">
<VListItem
v-for="(value, feature) in plan.features"
:key="feature as string"
class="px-0"
>
<template #prepend>
<VIcon icon="tabler-check" color="success" size="18" class="me-2" />
</template>
<VListItemTitle class="text-body-2">
<span class="font-weight-medium">{{ feature }}:</span> {{ value }}
</VListItemTitle>
</VListItem>
</VList>
</VCardText>
<VCardActions class="pa-4">
<span v-if="plan.stock_quantity !== null && plan.stock_quantity <= 0" class="text-body-2 font-weight-medium text-error w-100 text-center">
Out of Stock
</span>
<Link
v-else
:href="`/checkout/${plan.id}`"
class="text-decoration-none w-100"
>
<VBtn block>
Order Now
</VBtn>
</Link>
</VCardActions>
</VCard>
</VCol>
</VRow>
</div>
<div v-if="!plansByType || Object.keys(plansByType).length === 0" class="text-center py-12">
<div class="text-medium-emphasis">No plans are currently available.</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,68 @@
<script lang="ts" setup>
import { Link } from '@inertiajs/vue3'
import AccountLayout from '@/Layouts/AccountLayout.vue'
import { formatPrice } from '@/utils/resolvers'
import type { Plan } from '@/types'
interface Props {
plan: Plan
}
defineOptions({ layout: AccountLayout })
defineProps<Props>()
</script>
<template>
<div>
<div class="mb-4">
<Link href="/plans" class="text-primary text-body-2 text-decoration-none">&larr; Back to Plans</Link>
</div>
<VCard style="max-width: 600px;">
<VCardText>
<div class="text-h4 font-weight-bold">{{ plan.name }}</div>
<div v-if="plan.description" class="text-medium-emphasis mt-2">{{ plan.description }}</div>
<div class="mt-6">
<span class="text-h3 font-weight-bold">
{{ formatPrice(plan.price, plan.billing_cycle) }}
</span>
</div>
<div v-if="plan.features" class="mt-8">
<div class="text-h6 mb-3">Features</div>
<VList density="compact" class="pa-0">
<VListItem
v-for="(value, feature) in plan.features"
:key="feature as string"
class="px-0"
>
<template #prepend>
<VIcon icon="tabler-check" color="success" size="18" class="me-2" />
</template>
<VListItemTitle class="text-body-2">
<span class="font-weight-medium">{{ feature }}:</span> {{ value }}
</VListItemTitle>
</VListItem>
</VList>
</div>
<div class="mt-8">
<span v-if="plan.stock_quantity !== null && plan.stock_quantity <= 0" class="text-body-2 font-weight-medium text-error">
This plan is currently out of stock.
</span>
<Link
v-else
:href="`/checkout/${plan.id}`"
class="text-decoration-none"
>
<VBtn size="large">
Order Now
</VBtn>
</Link>
</div>
</VCardText>
</VCard>
</div>
</template>

View File

@@ -0,0 +1,83 @@
<script lang="ts" setup>
import { useForm } from '@inertiajs/vue3'
import AccountLayout from '@/Layouts/AccountLayout.vue'
import AppTextField from '@/Components/app-form-elements/AppTextField.vue'
import type { User } from '@/types'
interface Props {
user: User & { phone?: string; company?: string }
}
defineOptions({ layout: AccountLayout })
const props = defineProps<Props>()
const form = useForm({
name: props.user.name,
phone: props.user.phone || '',
company: props.user.company || '',
})
const submit = (): void => {
form.put('/profile')
}
</script>
<template>
<div style="max-width: 600px;">
<div class="text-h4 font-weight-bold mb-6">Profile Settings</div>
<VCard>
<VCardText>
<VForm @submit.prevent="submit">
<VRow>
<VCol cols="12">
<AppTextField
v-model="form.name"
label="Name"
type="text"
required
:error-messages="form.errors.name ? [form.errors.name] : []"
/>
</VCol>
<VCol cols="12">
<AppTextField
:model-value="user.email"
label="Email"
type="email"
disabled
/>
</VCol>
<VCol cols="12">
<AppTextField
v-model="form.phone"
label="Phone"
type="tel"
/>
</VCol>
<VCol cols="12">
<AppTextField
v-model="form.company"
label="Company"
type="text"
/>
</VCol>
<VCol cols="12">
<VBtn
type="submit"
:loading="form.processing"
:disabled="form.processing"
>
Save Changes
</VBtn>
</VCol>
</VRow>
</VForm>
</VCardText>
</VCard>
</div>
</template>

View File

@@ -0,0 +1,149 @@
<script lang="ts" setup>
import { ref } from 'vue'
import { useForm, usePage, router } from '@inertiajs/vue3'
import AccountLayout from '@/Layouts/AccountLayout.vue'
import AppTextField from '@/Components/app-form-elements/AppTextField.vue'
import type { SharedPageProps } from '@/types'
defineOptions({ layout: AccountLayout })
const page = usePage<SharedPageProps>()
const enabling = ref(false)
const confirming = ref(false)
const disabling = ref(false)
const qrCode = ref('')
const recoveryCodes = ref<string[]>([])
const confirmationForm = useForm({
code: '',
})
const enableTwoFactor = (): void => {
enabling.value = true
router.post('/user/two-factor-authentication', {}, {
preserveScroll: true,
onSuccess: () => {
confirming.value = true
showQrCode()
showRecoveryCodes()
},
onFinish: () => {
enabling.value = false
},
})
}
const showQrCode = (): void => {
fetch('/user/two-factor-qr-code')
.then(r => r.json())
.then(data => { qrCode.value = data.svg })
}
const showRecoveryCodes = (): void => {
fetch('/user/two-factor-recovery-codes')
.then(r => r.json())
.then(data => { recoveryCodes.value = data })
}
const confirmTwoFactor = (): void => {
confirmationForm.post('/user/confirmed-two-factor-authentication', {
preserveScroll: true,
onSuccess: () => {
confirming.value = false
},
})
}
const disableTwoFactor = (): void => {
disabling.value = true
router.delete('/user/two-factor-authentication', {
preserveScroll: true,
onSuccess: () => {
qrCode.value = ''
recoveryCodes.value = []
},
onFinish: () => {
disabling.value = false
},
})
}
</script>
<template>
<div style="max-width: 600px;">
<div class="text-h6 mb-4">Two-Factor Authentication</div>
<div class="text-body-2 text-medium-emphasis mb-6">
Add an extra layer of security to your account using a TOTP authenticator app.
</div>
<div v-if="!page.props.auth?.user?.two_factor_enabled">
<VBtn
:loading="enabling"
:disabled="enabling"
@click="enableTwoFactor"
>
Enable Two-Factor Authentication
</VBtn>
</div>
<VCard v-if="confirming" class="mt-6">
<VCardText>
<div class="text-body-2 text-medium-emphasis mb-4">
Scan this QR code with your authenticator app, then enter the code below to confirm.
</div>
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-if="qrCode" v-html="qrCode" class="mb-4" />
<VForm @submit.prevent="confirmTwoFactor" style="max-width: 300px;">
<AppTextField
v-model="confirmationForm.code"
label="Confirmation Code"
type="text"
inputmode="numeric"
placeholder="000000"
:error-messages="confirmationForm.errors.code ? [confirmationForm.errors.code] : []"
class="mb-4"
/>
<VBtn
type="submit"
:loading="confirmationForm.processing"
:disabled="confirmationForm.processing"
>
Confirm
</VBtn>
</VForm>
</VCardText>
</VCard>
<VCard v-if="recoveryCodes.length > 0 && !confirming" class="mt-6">
<VCardText>
<div class="text-subtitle-2 font-weight-bold mb-2">Recovery Codes</div>
<div class="text-body-2 text-medium-emphasis mb-3">
Store these codes in a safe place. They can be used to access your account if you lose your authenticator device.
</div>
<VSheet rounded color="surface-variant" class="pa-4 font-weight-medium" style="font-family: monospace;">
<div v-for="code in recoveryCodes" :key="code">{{ code }}</div>
</VSheet>
</VCardText>
</VCard>
<VCard v-if="page.props.auth?.user?.two_factor_enabled && !confirming" class="mt-6">
<VCardText>
<VAlert type="success" variant="tonal" class="mb-4">
Two-factor authentication is enabled.
</VAlert>
<VBtn
color="error"
:loading="disabling"
:disabled="disabling"
@click="disableTwoFactor"
>
Disable Two-Factor Authentication
</VBtn>
</VCardText>
</VCard>
</div>
</template>

View File

@@ -0,0 +1,78 @@
<script lang="ts" setup>
import { Link } from '@inertiajs/vue3'
import AccountLayout from '@/Layouts/AccountLayout.vue'
import { resolveSubscriptionStatusColor, formatPrice } from '@/utils/resolvers'
import type { Subscription } from '@/types'
interface Props {
subscriptions: Subscription[]
}
defineOptions({ layout: AccountLayout })
defineProps<Props>()
</script>
<template>
<div>
<div class="d-flex align-center justify-space-between mb-6">
<div class="text-h4 font-weight-bold">Subscriptions</div>
<Link href="/plans" class="text-decoration-none">
<VBtn>Browse Plans</VBtn>
</Link>
</div>
<VCard v-if="subscriptions.length === 0">
<VCardText class="text-center py-12">
<div class="text-medium-emphasis mb-4">You don't have any subscriptions yet.</div>
<Link href="/plans" class="text-primary text-body-2 font-weight-medium text-decoration-none">Browse Available Plans</Link>
</VCardText>
</VCard>
<div v-else class="d-flex flex-column ga-4">
<VCard
v-for="subscription in subscriptions"
:key="subscription.id"
>
<VCardText>
<div class="d-flex align-center justify-space-between">
<div>
<div class="text-h6 font-weight-bold">
{{ subscription.plan?.name || subscription.type }}
</div>
<div class="text-body-2 text-medium-emphasis mt-1">
{{ subscription.gateway || 'stripe' }} &middot;
<span v-if="subscription.current_period_end">
Renews {{ new Date(subscription.current_period_end).toLocaleDateString() }}
</span>
</div>
</div>
<div class="d-flex align-center ga-3">
<VChip
:color="resolveSubscriptionStatusColor(subscription.stripe_status)"
size="small"
class="text-capitalize"
>
{{ subscription.stripe_status }}
</VChip>
<Link
:href="`/subscriptions/${subscription.id}`"
class="text-primary text-body-2 font-weight-medium text-decoration-none"
>
Manage
</Link>
</div>
</div>
<div v-if="subscription.plan" class="text-body-2 text-medium-emphasis mt-3">
{{ formatPrice(subscription.plan.price, subscription.plan.billing_cycle) }}
</div>
<div v-if="subscription.ends_at" class="text-body-2 text-error mt-2">
Cancels on {{ new Date(subscription.ends_at).toLocaleDateString() }}
</div>
</VCardText>
</VCard>
</div>
</div>
</template>

View File

@@ -0,0 +1,173 @@
<script lang="ts" setup>
import { ref } from 'vue'
import { useForm, Link } from '@inertiajs/vue3'
import AccountLayout from '@/Layouts/AccountLayout.vue'
import { resolveSubscriptionStatusColor, formatPrice } from '@/utils/resolvers'
import type { Subscription, Plan } from '@/types'
interface Props {
subscription: Subscription
availablePlans: Plan[]
}
defineOptions({ layout: AccountLayout })
const props = defineProps<Props>()
const cancelImmediately = ref(false)
const cancelForm = useForm({
immediately: false,
})
const swapForm = useForm({
plan_id: '',
})
const cancelSubscription = (): void => {
cancelForm.immediately = cancelImmediately.value
cancelForm.post(`/subscriptions/${props.subscription.id}/cancel`)
}
const resumeSubscription = (): void => {
useForm({}).post(`/subscriptions/${props.subscription.id}/resume`)
}
const swapPlan = (): void => {
swapForm.post(`/subscriptions/${props.subscription.id}/swap`)
}
</script>
<template>
<div>
<div class="mb-4">
<Link href="/subscriptions" class="text-primary text-body-2 text-decoration-none">&larr; Back to Subscriptions</Link>
</div>
<div class="text-h4 font-weight-bold mb-6">Subscription Details</div>
<VRow>
<!-- Subscription Info -->
<VCol cols="12" lg="8">
<VCard class="mb-6">
<VCardText>
<div class="d-flex align-center justify-space-between mb-4">
<div class="text-h6 font-weight-bold">
{{ subscription.plan?.name || subscription.type }}
</div>
<VChip
:color="resolveSubscriptionStatusColor(subscription.stripe_status)"
size="small"
class="text-capitalize"
>
{{ subscription.stripe_status }}
</VChip>
</div>
<VRow>
<VCol cols="6">
<div class="text-body-2 text-medium-emphasis">Gateway</div>
<div class="text-body-1 text-capitalize mt-1">{{ subscription.gateway || 'stripe' }}</div>
</VCol>
<VCol v-if="subscription.plan" cols="6">
<div class="text-body-2 text-medium-emphasis">Price</div>
<div class="text-body-1 mt-1">{{ formatPrice(subscription.plan.price, subscription.plan.billing_cycle) }}</div>
</VCol>
<VCol v-if="subscription.current_period_start" cols="6">
<div class="text-body-2 text-medium-emphasis">Current Period Start</div>
<div class="text-body-1 mt-1">{{ new Date(subscription.current_period_start).toLocaleDateString() }}</div>
</VCol>
<VCol v-if="subscription.current_period_end" cols="6">
<div class="text-body-2 text-medium-emphasis">Current Period End</div>
<div class="text-body-1 mt-1">{{ new Date(subscription.current_period_end).toLocaleDateString() }}</div>
</VCol>
<VCol v-if="subscription.ends_at" cols="6">
<div class="text-body-2 text-medium-emphasis">Cancels On</div>
<div class="text-body-1 text-error mt-1">{{ new Date(subscription.ends_at).toLocaleDateString() }}</div>
</VCol>
<VCol cols="6">
<div class="text-body-2 text-medium-emphasis">Created</div>
<div class="text-body-1 mt-1">{{ new Date(subscription.created_at).toLocaleDateString() }}</div>
</VCol>
</VRow>
</VCardText>
</VCard>
<!-- Change Plan -->
<VCard v-if="availablePlans.length > 0 && subscription.stripe_status === 'active'">
<VCardTitle>Change Plan</VCardTitle>
<VCardText>
<VForm @submit.prevent="swapPlan">
<VRadioGroup v-model="swapForm.plan_id" class="mb-4">
<VRadio
v-for="plan in availablePlans"
:key="plan.id"
:value="String(plan.id)"
>
<template #label>
<div class="d-flex justify-space-between w-100">
<span>{{ plan.name }}</span>
<span class="text-medium-emphasis">{{ formatPrice(plan.price, plan.billing_cycle) }}</span>
</div>
</template>
</VRadio>
</VRadioGroup>
<VBtn
type="submit"
:loading="swapForm.processing"
:disabled="!swapForm.plan_id || swapForm.processing"
>
{{ swapForm.processing ? 'Changing...' : 'Change Plan' }}
</VBtn>
</VForm>
</VCardText>
</VCard>
</VCol>
<!-- Actions Sidebar -->
<VCol cols="12" lg="4">
<!-- Cancel -->
<VCard v-if="subscription.stripe_status === 'active' && !subscription.ends_at" class="mb-6">
<VCardTitle>Cancel Subscription</VCardTitle>
<VCardText>
<VCheckbox
v-model="cancelImmediately"
label="Cancel immediately (no grace period)"
hide-details
class="mb-4"
/>
<VBtn
color="error"
block
:loading="cancelForm.processing"
:disabled="cancelForm.processing"
@click="cancelSubscription"
>
{{ cancelForm.processing ? 'Cancelling...' : 'Cancel Subscription' }}
</VBtn>
</VCardText>
</VCard>
<!-- Resume -->
<VCard v-if="subscription.ends_at && subscription.stripe_status !== 'canceled'">
<VCardTitle>Resume Subscription</VCardTitle>
<VCardText>
<div class="text-body-2 text-medium-emphasis mb-3">
Your subscription is set to cancel. You can resume it before it expires.
</div>
<VBtn
color="success"
block
@click="resumeSubscription"
>
Resume Subscription
</VBtn>
</VCardText>
</VCard>
</VCol>
</VRow>
</div>
</template>