Update all 26 Vue files (3 layouts, 4 components, 19 pages) and the Blade root template to use a dark color scheme: gray-950 backgrounds, gray-900 cards, gray-800 borders, light text, and adjusted status badges/flash messages for dark backgrounds. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
192 lines
8.7 KiB
Vue
192 lines
8.7 KiB
Vue
<script setup>
|
|
import { ref, computed } from 'vue';
|
|
import { useForm, Link } from '@inertiajs/vue3';
|
|
import AppLayout from '@/Layouts/AppLayout.vue';
|
|
|
|
defineOptions({ layout: AppLayout });
|
|
|
|
const props = defineProps({
|
|
plan: Object,
|
|
paymentMethods: Array,
|
|
intent: Object,
|
|
stripeKey: String,
|
|
});
|
|
|
|
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 () => {
|
|
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"]')?.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 = () => {
|
|
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-sm text-blue-400 hover:text-blue-300">← Back to Plans</Link>
|
|
</div>
|
|
|
|
<h1 class="text-2xl font-bold text-white mb-6">Checkout</h1>
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
<!-- Order Summary -->
|
|
<div class="lg:col-span-1 order-2 lg:order-1">
|
|
<div class="bg-gray-900 rounded-lg border border-gray-800 shadow-sm p-6">
|
|
<h2 class="text-lg font-semibold text-white mb-4">Order Summary</h2>
|
|
|
|
<div class="space-y-3">
|
|
<div class="flex justify-between text-sm">
|
|
<span class="text-gray-400">{{ plan.name }}</span>
|
|
<span class="text-white">${{ parseFloat(plan.price).toFixed(2) }}</span>
|
|
</div>
|
|
<div class="flex justify-between text-sm text-gray-500">
|
|
<span>Billing Cycle</span>
|
|
<span class="capitalize">{{ plan.billing_cycle }}</span>
|
|
</div>
|
|
<div v-if="couponApplied" class="flex justify-between text-sm text-green-400">
|
|
<span>Discount</span>
|
|
<span>-${{ couponDiscount.toFixed(2) }}</span>
|
|
</div>
|
|
<hr class="border-gray-800">
|
|
<div class="flex justify-between font-semibold text-white">
|
|
<span>Total</span>
|
|
<span>${{ total }}/{{ plan.billing_cycle }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Checkout Form -->
|
|
<div class="lg:col-span-2 order-1 lg:order-2">
|
|
<form @submit.prevent="submit" class="space-y-6">
|
|
<!-- Payment Gateway -->
|
|
<div class="bg-gray-900 rounded-lg border border-gray-800 shadow-sm p-6">
|
|
<h2 class="text-lg font-semibold text-white mb-4">Payment Method</h2>
|
|
|
|
<div class="space-y-3">
|
|
<label class="flex items-center p-3 border rounded-md cursor-pointer" :class="selectedGateway === 'stripe' ? 'border-blue-500 bg-blue-900/20' : 'border-gray-700'">
|
|
<input v-model="selectedGateway" type="radio" value="stripe" class="mr-3">
|
|
<span class="text-sm font-medium text-gray-200">Credit / Debit Card (Stripe)</span>
|
|
</label>
|
|
<label class="flex items-center p-3 border rounded-md cursor-pointer" :class="selectedGateway === 'paypal' ? 'border-blue-500 bg-blue-900/20' : 'border-gray-700'">
|
|
<input v-model="selectedGateway" type="radio" value="paypal" class="mr-3">
|
|
<span class="text-sm font-medium text-gray-200">PayPal</span>
|
|
</label>
|
|
</div>
|
|
|
|
<!-- Saved Payment Methods (Stripe) -->
|
|
<div v-if="selectedGateway === 'stripe' && paymentMethods.length > 0" class="mt-4">
|
|
<label class="block text-sm font-medium text-gray-300 mb-2">Select Card</label>
|
|
<select v-model="selectedPaymentMethod" class="w-full rounded-md bg-gray-800 border-gray-700 text-gray-100 text-sm">
|
|
<option v-for="pm in paymentMethods" :key="pm.id" :value="pm.id">
|
|
{{ pm.brand }} ending in {{ pm.last_four }} ({{ pm.exp_month }}/{{ pm.exp_year }})
|
|
<template v-if="pm.is_default"> - Default</template>
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div v-if="selectedGateway === 'stripe' && paymentMethods.length === 0" class="mt-4">
|
|
<p class="text-sm text-gray-500">
|
|
You have no saved payment methods.
|
|
<Link href="/billing" class="text-blue-400 hover:text-blue-300">Add one first</Link>.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Coupon -->
|
|
<div class="bg-gray-900 rounded-lg border border-gray-800 shadow-sm p-6">
|
|
<h2 class="text-lg font-semibold text-white mb-4">Coupon Code</h2>
|
|
|
|
<div class="flex gap-3">
|
|
<input
|
|
v-model="couponCode"
|
|
type="text"
|
|
placeholder="Enter coupon code"
|
|
class="flex-1 rounded-md bg-gray-800 border-gray-700 text-gray-100 text-sm placeholder-gray-500"
|
|
:disabled="couponApplied"
|
|
>
|
|
<button
|
|
type="button"
|
|
@click="applyCoupon"
|
|
:disabled="!couponCode || couponApplied"
|
|
class="px-4 py-2 bg-gray-800 text-sm font-medium text-gray-300 rounded-md hover:bg-gray-700 border border-gray-700 disabled:opacity-50"
|
|
>
|
|
{{ couponApplied ? 'Applied' : 'Apply' }}
|
|
</button>
|
|
</div>
|
|
<p v-if="couponError" class="mt-2 text-sm text-red-400">{{ couponError }}</p>
|
|
<p v-if="couponApplied" class="mt-2 text-sm text-green-400">Coupon applied successfully!</p>
|
|
</div>
|
|
|
|
<!-- Errors -->
|
|
<div v-if="form.errors && Object.keys(form.errors).length" class="rounded-md bg-red-900/50 border border-red-800 p-4">
|
|
<ul class="list-disc list-inside text-sm text-red-300">
|
|
<li v-for="(error, field) in form.errors" :key="field">{{ error }}</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Submit -->
|
|
<button
|
|
type="submit"
|
|
:disabled="form.processing || (selectedGateway === 'stripe' && !selectedPaymentMethod)"
|
|
class="w-full px-6 py-3 bg-blue-600 text-white text-sm font-medium rounded-md hover:bg-blue-700 disabled:opacity-50"
|
|
>
|
|
<span v-if="form.processing">Processing...</span>
|
|
<span v-else>Subscribe for ${{ total }}/{{ plan.billing_cycle }}</span>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|