From dd1a5d7ffca96c8154204cd949ac2d30524ff70b4bdd042d6112ffe2e38b2ff7 Mon Sep 17 00:00:00 2001 From: Claude Dev Date: Sat, 14 Mar 2026 17:09:15 -0400 Subject: [PATCH] Replace AppTextField/AppSelect/AppTextarea with native Vuetify equivalents across 22 pages Vuetify defaults already configure VTextField/VSelect/VTextarea with the correct settings (outlined variant, comfortable density, primary color, auto hideDetails), making the wrapper components unnecessary. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../ts/Pages/Admin/Coupons/Create.vue | 14 +- .../resources/ts/Pages/Admin/Coupons/Edit.vue | 14 +- .../ts/Pages/Admin/Customers/Edit.vue | 15 +- .../ts/Pages/Admin/EmailTemplates/Edit.vue | 399 ++++++++++++++++++ .../ts/Pages/Admin/Invoices/Create.vue | 12 +- .../ts/Pages/Admin/Invoices/Edit.vue | 12 +- .../resources/ts/Pages/Admin/Orders/Show.vue | 3 +- .../resources/ts/Pages/Admin/Plans/Create.vue | 23 +- .../resources/ts/Pages/Admin/Plans/Edit.vue | 23 +- .../ts/Pages/Admin/Settings/Index.vue | 88 ++-- .../ts/Pages/Admin/TaxRates/Create.vue | 196 +++++++++ .../ts/Pages/Admin/TaxRates/Edit.vue | 232 ++++++++++ .../ts/Pages/Admin/TaxRates/Index.vue | 279 ++++++++++++ .../resources/ts/Pages/Admin/Tickets/Show.vue | 6 +- website/resources/ts/Pages/Checkout/Show.vue | 15 +- .../resources/ts/Pages/Profile/AccountTab.vue | 24 +- .../resources/ts/Pages/Profile/BillingTab.vue | 18 +- .../ts/Pages/Profile/SecurityTab.vue | 9 +- .../ts/Pages/Profile/TwoFactorSetup.vue | 3 +- .../resources/ts/Pages/Subscriptions/Show.vue | 4 +- website/resources/ts/Pages/Tickets/Create.vue | 11 +- website/resources/ts/Pages/Tickets/Show.vue | 3 +- 22 files changed, 1236 insertions(+), 167 deletions(-) create mode 100644 website/resources/ts/Pages/Admin/EmailTemplates/Edit.vue create mode 100644 website/resources/ts/Pages/Admin/TaxRates/Create.vue create mode 100644 website/resources/ts/Pages/Admin/TaxRates/Edit.vue create mode 100644 website/resources/ts/Pages/Admin/TaxRates/Index.vue diff --git a/website/resources/ts/Pages/Admin/Coupons/Create.vue b/website/resources/ts/Pages/Admin/Coupons/Create.vue index efe0252..8da9929 100644 --- a/website/resources/ts/Pages/Admin/Coupons/Create.vue +++ b/website/resources/ts/Pages/Admin/Coupons/Create.vue @@ -2,8 +2,6 @@ import { Link, useForm } from '@inertiajs/vue3' import { computed } from 'vue' import AdminLayout from '@/Layouts/AdminLayout.vue' -import AppTextField from '@/Components/app-form-elements/AppTextField.vue' -import AppSelect from '@/Components/app-form-elements/AppSelect.vue' interface PlanOption { id: number @@ -95,7 +93,7 @@ function submit(): void { - - - Optionally restrict this coupon to specific plans. Leave empty to apply to all plans.

- - - - - - Optionally restrict this coupon to specific plans. Leave empty to apply to all plans.

- - - import { Link, useForm } from '@inertiajs/vue3' import AdminLayout from '@/Layouts/AdminLayout.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' import type { User } from '@/types' defineOptions({ layout: AdminLayout }) @@ -59,7 +56,7 @@ function submit(): void { - - - - - - +import { Link, useForm, router } from '@inertiajs/vue3' +import { ref, computed } from 'vue' +import AdminLayout from '@/Layouts/AdminLayout.vue' + +interface EmailTemplate { + id: number + slug: string + name: string + subject: string + body: string + available_variables: string[] + is_active: boolean + updated_at: string +} + +interface Props { + template: EmailTemplate +} + +defineOptions({ layout: AdminLayout }) + +const props = defineProps() + +const form = useForm({ + subject: props.template.subject, + body: props.template.body, + is_active: props.template.is_active, +}) + +const showPreviewDialog = ref(false) +const previewLoading = ref(false) +const previewSubject = ref('') +const previewBody = ref('') +const showResetDialog = ref(false) + +const bodyTextareaRef = ref | null>(null) + +function insertVariable(variable: string): void { + const placeholder = `{{${variable}}}` + + // Find the textarea element within the VTextarea component + const textareaEl = document.querySelector('.body-editor textarea') as HTMLTextAreaElement | null + + if (textareaEl) { + const start = textareaEl.selectionStart + const end = textareaEl.selectionEnd + const currentValue = form.body + + form.body = currentValue.substring(0, start) + placeholder + currentValue.substring(end) + + // Set cursor position after inserted variable + const newPosition = start + placeholder.length + requestAnimationFrame(() => { + textareaEl.focus() + textareaEl.setSelectionRange(newPosition, newPosition) + }) + } + else { + // Fallback: append to end + form.body += placeholder + } +} + +async function loadPreview(): Promise { + previewLoading.value = true + showPreviewDialog.value = true + + try { + const response = await fetch(`/email-templates/${props.template.id}/preview`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': (document.querySelector('meta[name="csrf-token"]') as HTMLMetaElement)?.content ?? '', + 'Accept': 'application/json', + }, + }) + + if (response.ok) { + const data = await response.json() + previewSubject.value = data.subject + previewBody.value = data.body + } + } + catch { + previewSubject.value = 'Preview failed' + previewBody.value = 'Unable to load preview.' + } + finally { + previewLoading.value = false + } +} + +function confirmReset(): void { + showResetDialog.value = true +} + +function resetToDefault(): void { + router.post(`/email-templates/${props.template.id}/reset`, {}, { + preserveScroll: true, + onSuccess: () => { + showResetDialog.value = false + }, + }) +} + +function submit(): void { + form.put(`/email-templates/${props.template.id}`, { + preserveScroll: true, + }) +} + +const formattedUpdatedAt = computed(() => { + const date = new Date(props.template.updated_at) + return date.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }) +}) + + + diff --git a/website/resources/ts/Pages/Admin/Invoices/Create.vue b/website/resources/ts/Pages/Admin/Invoices/Create.vue index 5ee9c47..4873552 100644 --- a/website/resources/ts/Pages/Admin/Invoices/Create.vue +++ b/website/resources/ts/Pages/Admin/Invoices/Create.vue @@ -2,8 +2,6 @@ import { Link, useForm } from '@inertiajs/vue3' import { computed } from 'vue' import AdminLayout from '@/Layouts/AdminLayout.vue' -import AppTextField from '@/Components/app-form-elements/AppTextField.vue' -import AppTextarea from '@/Components/app-form-elements/AppTextarea.vue' import { formatPrice } from '@/utils/resolvers' interface CustomerOption { @@ -135,7 +133,7 @@ function submitAndSend(): void { > - - - - - - - - - - { - - - - - - - - - - - (() => { - (() => { /> - (() => { /> - (() => { - (() => { /> - (() => { /> - (() => { > - (() => { /> - (() => { - (() => { :error-messages="form.errors.stock_quantity" class="mb-4" /> - {
- { - { - { - { - { /> - { @click="showVirtfusionToken = !showVirtfusionToken" /> - + @@ -422,7 +420,7 @@ async function testDiscordWebhook(channel: string): Promise { - { /> - { @click="showPterodactylToken = !showPterodactylToken" /> - + @@ -478,7 +476,7 @@ async function testDiscordWebhook(channel: string): Promise { - { /> - { @click="showSynergycpToken = !showSynergycpToken" /> - + @@ -534,7 +532,7 @@ async function testDiscordWebhook(channel: string): Promise { - { /> - { @click="showEnhanceToken = !showEnhanceToken" /> - + - { @click="showEnhanceOrgId = !showEnhanceOrgId" /> - + @@ -589,7 +587,7 @@ async function testDiscordWebhook(channel: string): Promise { - { /> - { - { /> - { > {{ discordTestResults.payment.message }} - { - + @@ -748,7 +746,7 @@ async function testDiscordWebhook(channel: string): Promise { > {{ discordTestResults.provisioning.message }} - { - + @@ -801,7 +799,7 @@ async function testDiscordWebhook(channel: string): Promise { > {{ discordTestResults.support.message }} - { - + @@ -854,7 +852,7 @@ async function testDiscordWebhook(channel: string): Promise { > {{ discordTestResults.system.message }} - { - + @@ -890,7 +888,7 @@ async function testDiscordWebhook(channel: string): Promise { - { - { Days after invoice due date before suspension warning is sent - + - { Days after warning before service is suspended - + - { Days after suspension before service is automatically terminated - + @@ -994,7 +992,7 @@ async function testDiscordWebhook(channel: string): Promise { - { Price charged per GB over the plan's included bandwidth - + - { Days after overage detected before billing begins - + @@ -1194,7 +1192,7 @@ async function testDiscordWebhook(channel: string): Promise { - { - +import { Link, useForm } from '@inertiajs/vue3' +import { computed } from 'vue' +import AdminLayout from '@/Layouts/AdminLayout.vue' + +defineOptions({ layout: AdminLayout }) + +const countryOptions = [ + { title: 'United States (US)', value: 'US' }, + { title: 'United Kingdom (GB)', value: 'GB' }, + { title: 'Germany (DE)', value: 'DE' }, + { title: 'France (FR)', value: 'FR' }, + { title: 'Canada (CA)', value: 'CA' }, + { title: 'Australia (AU)', value: 'AU' }, + { title: 'Netherlands (NL)', value: 'NL' }, + { title: 'Sweden (SE)', value: 'SE' }, + { title: 'Japan (JP)', value: 'JP' }, + { title: 'Brazil (BR)', value: 'BR' }, + { title: 'Italy (IT)', value: 'IT' }, + { title: 'Spain (ES)', value: 'ES' }, + { title: 'Ireland (IE)', value: 'IE' }, + { title: 'New Zealand (NZ)', value: 'NZ' }, + { title: 'Singapore (SG)', value: 'SG' }, + { title: 'India (IN)', value: 'IN' }, + { title: 'Switzerland (CH)', value: 'CH' }, + { title: 'Austria (AT)', value: 'AT' }, + { title: 'Belgium (BE)', value: 'BE' }, + { title: 'Portugal (PT)', value: 'PT' }, + { title: 'Denmark (DK)', value: 'DK' }, + { title: 'Norway (NO)', value: 'NO' }, + { title: 'Finland (FI)', value: 'FI' }, + { title: 'Poland (PL)', value: 'PL' }, + { title: 'Czech Republic (CZ)', value: 'CZ' }, + { title: 'Mexico (MX)', value: 'MX' }, +] + +const typeOptions = [ + { title: 'Exclusive (added on top of price)', value: 'exclusive' }, + { title: 'Inclusive (included in price)', value: 'inclusive' }, +] + +const form = useForm({ + name: '', + country_code: '' as string, + region_code: '' as string, + rate: '' as string | number, + type: 'exclusive' as string, + priority: 0, + is_active: true, +}) + +const typeHint = computed(() => { + if (form.type === 'inclusive') { + return 'Tax is already included in the displayed price.' + } + return 'Tax will be added on top of the displayed price at checkout.' +}) + +function submit(): void { + form.post('/tax-rates', { + preserveScroll: true, + }) +} + + + diff --git a/website/resources/ts/Pages/Admin/TaxRates/Edit.vue b/website/resources/ts/Pages/Admin/TaxRates/Edit.vue new file mode 100644 index 0000000..7488dc9 --- /dev/null +++ b/website/resources/ts/Pages/Admin/TaxRates/Edit.vue @@ -0,0 +1,232 @@ + + + diff --git a/website/resources/ts/Pages/Admin/TaxRates/Index.vue b/website/resources/ts/Pages/Admin/TaxRates/Index.vue new file mode 100644 index 0000000..db7c067 --- /dev/null +++ b/website/resources/ts/Pages/Admin/TaxRates/Index.vue @@ -0,0 +1,279 @@ + + + diff --git a/website/resources/ts/Pages/Admin/Tickets/Show.vue b/website/resources/ts/Pages/Admin/Tickets/Show.vue index b81f9cb..bf630eb 100644 --- a/website/resources/ts/Pages/Admin/Tickets/Show.vue +++ b/website/resources/ts/Pages/Admin/Tickets/Show.vue @@ -2,8 +2,6 @@ import { useForm, Link } from '@inertiajs/vue3' import { ref } from 'vue' import AdminLayout from '@/Layouts/AdminLayout.vue' -import AppTextarea from '@/Components/app-form-elements/AppTextarea.vue' -import AppSelect from '@/Components/app-form-elements/AppSelect.vue' import { resolveTicketStatusColor, resolveTicketPriorityColor } from '@/utils/resolvers' import type { SupportTicket } from '@/types' @@ -207,7 +205,7 @@ function getUserInitial(name: string): string { -
- {
- { - +
@@ -774,7 +771,7 @@ onUnmounted(() => { - {
- Coupon Code
- { - + import { ref, computed } from 'vue' import { useForm } from '@inertiajs/vue3' -import AppTextField from '@/Components/app-form-elements/AppTextField.vue' -import AppSelect from '@/Components/app-form-elements/AppSelect.vue' import type { User, UserProfile } from '@/types' interface Props { @@ -116,7 +114,7 @@ const resetForm = (): void => { md="6" cols="12" > - { md="6" cols="12" > - { cols="12" md="6" > - { cols="12" md="6" > - { cols="12" md="6" > - { cols="12" md="6" > - { cols="12" md="6" > - { cols="12" md="6" > - { cols="12" md="6" > - { cols="12" md="6" > - { cols="12" md="6" > - import { Link } from '@inertiajs/vue3' import { useForm } from '@inertiajs/vue3' -import AppTextField from '@/Components/app-form-elements/AppTextField.vue' -import AppSelect from '@/Components/app-form-elements/AppSelect.vue' import type { UserProfile } from '@/types' interface Props { @@ -84,7 +82,7 @@ const resetBillingForm = (): void => { - { - { cols="12" md="6" > - { cols="12" md="6" > - { cols="12" md="6" > - { cols="12" md="6" > - { cols="12" md="6" > - { cols="12" md="6" > - import { ref } from 'vue' import { useForm, usePage, router } from '@inertiajs/vue3' -import AppTextField from '@/Components/app-form-elements/AppTextField.vue' import type { SharedPageProps } from '@/types' interface Props { @@ -117,7 +116,7 @@ const disableTwoFactor = (): void => { cols="12" md="6" > - { cols="12" md="6" > - { cols="12" md="6" > - { @submit.prevent="confirmTwoFactor" style="max-width: 300px;" > - {
- (() => props.subscription.stripe_status ===
- 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 AppTextarea from '@/Components/app-form-elements/AppTextarea.vue' defineOptions({ layout: AccountLayout }) @@ -52,7 +49,7 @@ function submitTicket(): void { - - - - -