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:
49
website/resources/ts/Pages/Admin/Dashboard.vue
Normal file
49
website/resources/ts/Pages/Admin/Dashboard.vue
Normal 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>
|
||||
57
website/resources/ts/Pages/Auth/ConfirmPassword.vue
Normal file
57
website/resources/ts/Pages/Auth/ConfirmPassword.vue
Normal 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>
|
||||
84
website/resources/ts/Pages/Auth/ForgotPassword.vue
Normal file
84
website/resources/ts/Pages/Auth/ForgotPassword.vue
Normal 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>
|
||||
118
website/resources/ts/Pages/Auth/Login.vue
Normal file
118
website/resources/ts/Pages/Auth/Login.vue
Normal 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>
|
||||
129
website/resources/ts/Pages/Auth/Register.vue
Normal file
129
website/resources/ts/Pages/Auth/Register.vue
Normal 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">&</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>
|
||||
94
website/resources/ts/Pages/Auth/ResetPassword.vue
Normal file
94
website/resources/ts/Pages/Auth/ResetPassword.vue
Normal 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>
|
||||
93
website/resources/ts/Pages/Auth/TwoFactorChallenge.vue
Normal file
93
website/resources/ts/Pages/Auth/TwoFactorChallenge.vue
Normal 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>
|
||||
51
website/resources/ts/Pages/Auth/VerifyEmail.vue
Normal file
51
website/resources/ts/Pages/Auth/VerifyEmail.vue
Normal 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>
|
||||
168
website/resources/ts/Pages/Billing/Index.vue
Normal file
168
website/resources/ts/Pages/Billing/Index.vue
Normal 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">•••• {{ 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>
|
||||
88
website/resources/ts/Pages/Billing/Invoices.vue
Normal file
88
website/resources/ts/Pages/Billing/Invoices.vue
Normal 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">← 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>
|
||||
86
website/resources/ts/Pages/Billing/Transactions.vue
Normal file
86
website/resources/ts/Pages/Billing/Transactions.vue
Normal 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">← 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>
|
||||
199
website/resources/ts/Pages/Checkout/Show.vue
Normal file
199
website/resources/ts/Pages/Checkout/Show.vue
Normal 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">← 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>
|
||||
73
website/resources/ts/Pages/Dashboard.vue
Normal file
73
website/resources/ts/Pages/Dashboard.vue
Normal 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 & 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>
|
||||
108
website/resources/ts/Pages/Marketing/About.vue
Normal file
108
website/resources/ts/Pages/Marketing/About.vue
Normal 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>
|
||||
153
website/resources/ts/Pages/Marketing/Contact.vue
Normal file
153
website/resources/ts/Pages/Marketing/Contact.vue
Normal 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>
|
||||
304
website/resources/ts/Pages/Marketing/DedicatedServers.vue
Normal file
304
website/resources/ts/Pages/Marketing/DedicatedServers.vue
Normal 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>
|
||||
97
website/resources/ts/Pages/Marketing/GameServers.vue
Normal file
97
website/resources/ts/Pages/Marketing/GameServers.vue
Normal 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>
|
||||
113
website/resources/ts/Pages/Marketing/Home.vue
Normal file
113
website/resources/ts/Pages/Marketing/Home.vue
Normal 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>
|
||||
381
website/resources/ts/Pages/Marketing/Pricing.vue
Normal file
381
website/resources/ts/Pages/Marketing/Pricing.vue
Normal 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>
|
||||
102
website/resources/ts/Pages/Marketing/Products.vue
Normal file
102
website/resources/ts/Pages/Marketing/Products.vue
Normal 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>
|
||||
169
website/resources/ts/Pages/Marketing/VpsHosting.vue
Normal file
169
website/resources/ts/Pages/Marketing/VpsHosting.vue
Normal 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>
|
||||
253
website/resources/ts/Pages/Marketing/WebHosting.vue
Normal file
253
website/resources/ts/Pages/Marketing/WebHosting.vue
Normal 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>
|
||||
92
website/resources/ts/Pages/Plans/Index.vue
Normal file
92
website/resources/ts/Pages/Plans/Index.vue
Normal 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 & 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>
|
||||
68
website/resources/ts/Pages/Plans/Show.vue
Normal file
68
website/resources/ts/Pages/Plans/Show.vue
Normal 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">← 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>
|
||||
83
website/resources/ts/Pages/Profile/Show.vue
Normal file
83
website/resources/ts/Pages/Profile/Show.vue
Normal 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>
|
||||
149
website/resources/ts/Pages/Profile/TwoFactorSetup.vue
Normal file
149
website/resources/ts/Pages/Profile/TwoFactorSetup.vue
Normal 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>
|
||||
78
website/resources/ts/Pages/Subscriptions/Index.vue
Normal file
78
website/resources/ts/Pages/Subscriptions/Index.vue
Normal 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' }} ·
|
||||
<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>
|
||||
173
website/resources/ts/Pages/Subscriptions/Show.vue
Normal file
173
website/resources/ts/Pages/Subscriptions/Show.vue
Normal 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">← 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>
|
||||
Reference in New Issue
Block a user