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:
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