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

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

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

View File

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