adding a cart
This commit is contained in:
+119
-32
@@ -1,5 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { Head } from '@inertiajs/vue3';
|
||||
import { useHttp } from '@inertiajs/vue3';
|
||||
import { store, update, destroy } from '@/actions/App/Http/Controllers/CartController';
|
||||
|
||||
interface Product {
|
||||
id: number;
|
||||
@@ -9,12 +12,56 @@ interface Product {
|
||||
image_url: string | null;
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
interface CartItem {
|
||||
product_id: number;
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
products: Product[];
|
||||
cart: CartItem[];
|
||||
}>();
|
||||
|
||||
const cartItems = ref<CartItem[]>(props.cart);
|
||||
|
||||
const addHttp = useHttp({ product_id: 0 });
|
||||
const removeHttp = useHttp({});
|
||||
|
||||
function addToCart(product: Product): void {
|
||||
addHttp.product_id = product.id;
|
||||
addHttp.post(store.url(), {
|
||||
onSuccess: (data: unknown) => {
|
||||
cartItems.value = data as CartItem[];
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function removeFromCart(productId: number): void {
|
||||
removeHttp.delete(destroy.url(productId), {
|
||||
onSuccess: (data: unknown) => {
|
||||
cartItems.value = data as CartItem[];
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const cartWithDetails = computed(() =>
|
||||
cartItems.value.map((item) => ({
|
||||
...item,
|
||||
product: props.products.find((p) => p.id === item.product_id)!,
|
||||
})),
|
||||
);
|
||||
|
||||
const subtotal = computed(() =>
|
||||
cartWithDetails.value.reduce(
|
||||
(sum, item) => sum + item.product.price_cents * item.quantity,
|
||||
0,
|
||||
),
|
||||
);
|
||||
|
||||
function formatPrice(cents: number): string {
|
||||
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(cents / 100);
|
||||
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(
|
||||
cents / 100,
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -23,39 +70,79 @@ function formatPrice(cents: number): string {
|
||||
<link rel="preconnect" href="https://rsms.me/" />
|
||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
||||
</Head>
|
||||
<div class="min-h-screen bg-[#FDFDFC] p-6 text-[#1b1b18] dark:bg-[#0a0a0a] dark:text-[#FDFDFC] lg:p-8">
|
||||
<header class="mx-auto mb-8 max-w-4xl">
|
||||
<h1 class="text-2xl font-semibold">Packages</h1>
|
||||
</header>
|
||||
<main class="mx-auto max-w-4xl">
|
||||
<p v-if="products.length === 0" class="text-gray-500">No packages available.</p>
|
||||
<div v-else class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div
|
||||
v-for="product in products"
|
||||
:key="product.id"
|
||||
class="flex flex-col overflow-hidden rounded-lg border border-gray-200 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-800"
|
||||
>
|
||||
<img
|
||||
v-if="product.image_url"
|
||||
:src="product.image_url"
|
||||
:alt="product.name"
|
||||
class="h-48 w-full object-cover"
|
||||
/>
|
||||
<div v-else class="flex h-48 items-center justify-center bg-gray-100 dark:bg-gray-700">
|
||||
<span class="text-sm text-gray-400">No image</span>
|
||||
</div>
|
||||
<div class="flex flex-1 flex-col p-5">
|
||||
<h2 class="mb-1 font-medium capitalize">{{ product.name }}</h2>
|
||||
<p class="mb-4 grow text-sm text-gray-500 dark:text-gray-400">{{ product.description }}</p>
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-lg font-semibold">{{ formatPrice(product.price_cents) }}</p>
|
||||
<button type="button" class="rounded-md bg-gray-900 px-3 py-1.5 text-sm font-medium text-white hover:bg-gray-700 dark:bg-white dark:text-gray-900 dark:hover:bg-gray-200">
|
||||
Add
|
||||
</button>
|
||||
<div class="flex min-h-screen bg-[#FDFDFC] text-[#1b1b18] dark:bg-[#0a0a0a] dark:text-[#FDFDFC]">
|
||||
<div class="flex-1 p-6 lg:p-8">
|
||||
<header class="mb-8">
|
||||
<h1 class="text-2xl font-semibold">Packages</h1>
|
||||
</header>
|
||||
<main>
|
||||
<p v-if="products.length === 0" class="text-gray-500">No packages available.</p>
|
||||
<div v-else class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div
|
||||
v-for="product in products"
|
||||
:key="product.id"
|
||||
class="flex flex-col overflow-hidden rounded-lg border border-gray-200 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-800"
|
||||
>
|
||||
<img
|
||||
v-if="product.image_url"
|
||||
:src="product.image_url"
|
||||
:alt="product.name"
|
||||
class="h-96 w-full object-cover"
|
||||
/>
|
||||
<div v-else class="flex h-48 items-center justify-center bg-gray-100 dark:bg-gray-700">
|
||||
<span class="text-sm text-gray-400">No image</span>
|
||||
</div>
|
||||
<div class="flex flex-1 flex-col p-5">
|
||||
<h2 class="mb-1 font-medium capitalize">{{ product.name }}</h2>
|
||||
<p class="mb-4 grow text-sm text-gray-500 dark:text-gray-400">{{ product.description }}</p>
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-lg font-semibold">{{ formatPrice(product.price_cents) }}</p>
|
||||
<button
|
||||
type="button"
|
||||
:disabled="addHttp.processing"
|
||||
class="rounded-md bg-gray-900 px-3 py-1.5 text-sm font-medium text-white hover:bg-gray-700 disabled:opacity-50 dark:bg-white dark:text-gray-900 dark:hover:bg-gray-200"
|
||||
@click="addToCart(product)"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<aside class="w-[300px] shrink-0 border-l border-gray-200 p-6 dark:border-gray-700">
|
||||
<h2 class="mb-4 text-lg font-semibold">Cart</h2>
|
||||
<p v-if="cartItems.length === 0" class="text-sm text-gray-500">Your cart is empty.</p>
|
||||
<ul v-else class="mb-6 space-y-3">
|
||||
<li
|
||||
v-for="item in cartWithDetails"
|
||||
:key="item.product_id"
|
||||
class="flex items-center justify-between text-sm"
|
||||
>
|
||||
<span class="capitalize">
|
||||
{{ item.product.name }}
|
||||
<span class="text-gray-400">×{{ item.quantity }}</span>
|
||||
</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium">{{ formatPrice(item.product.price_cents * item.quantity) }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="text-gray-400 hover:text-red-500"
|
||||
@click="removeFromCart(item.product_id)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="border-t border-gray-200 pt-4 dark:border-gray-700">
|
||||
<div class="flex items-center justify-between font-semibold">
|
||||
<span>Subtotal</span>
|
||||
<span>{{ formatPrice(subtotal) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</aside>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user