adding a cart
This commit is contained in:
@@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Requests\StoreCartRequest;
|
||||||
|
use App\Http\Requests\UpdateCartRequest;
|
||||||
|
use App\Models\Cart;
|
||||||
|
use App\Models\Product;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class CartController extends Controller
|
||||||
|
{
|
||||||
|
public function store(StoreCartRequest $request): JsonResponse
|
||||||
|
{
|
||||||
|
$cart = $this->resolveCart();
|
||||||
|
|
||||||
|
$existing = $cart->products()->wherePivot('product_id', $request->product_id)->first();
|
||||||
|
|
||||||
|
if ($existing) {
|
||||||
|
$cart->products()->updateExistingPivot($request->product_id, [
|
||||||
|
'quantity' => $existing->pivot->quantity + 1,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$cart->products()->attach($request->product_id, ['quantity' => 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json($this->cartItems($cart));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(UpdateCartRequest $request, Product $product): JsonResponse
|
||||||
|
{
|
||||||
|
$cart = $this->resolveCart();
|
||||||
|
|
||||||
|
$cart->products()->updateExistingPivot($product->id, [
|
||||||
|
'quantity' => $request->quantity,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json($this->cartItems($cart));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Request $request, Product $product): JsonResponse
|
||||||
|
{
|
||||||
|
$cart = $this->resolveCart();
|
||||||
|
|
||||||
|
$cart->products()->detach($product->id);
|
||||||
|
|
||||||
|
return response()->json($this->cartItems($cart));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveCart(): Cart
|
||||||
|
{
|
||||||
|
$cartId = session('cart_id');
|
||||||
|
|
||||||
|
if ($cartId) {
|
||||||
|
$cart = Cart::find($cartId);
|
||||||
|
if ($cart) {
|
||||||
|
return $cart;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$cart = Cart::create(['user_id' => request()->user()?->id]);
|
||||||
|
session(['cart_id' => $cart->id]);
|
||||||
|
|
||||||
|
return $cart;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, array{product_id: int, quantity: int}>
|
||||||
|
*/
|
||||||
|
private function cartItems(Cart $cart): array
|
||||||
|
{
|
||||||
|
return $cart->products()->get()->map(fn ($p) => [
|
||||||
|
'product_id' => $p->id,
|
||||||
|
'quantity' => $p->pivot->quantity,
|
||||||
|
])->all();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class StoreCartRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'product_id' => ['required', 'integer', 'exists:products,id'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class UpdateCartRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'quantity' => ['required', 'integer', 'min:1'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Database\Factories\CartFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
|
|
||||||
|
class Cart extends Model
|
||||||
|
{
|
||||||
|
/** @use HasFactory<CartFactory> */
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = ['user_id'];
|
||||||
|
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function products(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Product::class)->withPivot('quantity');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ use Database\Factories\UserFactory;
|
|||||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
use Laravel\Fortify\TwoFactorAuthenticatable;
|
use Laravel\Fortify\TwoFactorAuthenticatable;
|
||||||
@@ -18,6 +19,11 @@ class User extends Authenticatable
|
|||||||
/** @use HasFactory<UserFactory> */
|
/** @use HasFactory<UserFactory> */
|
||||||
use HasFactory, Notifiable, TwoFactorAuthenticatable;
|
use HasFactory, Notifiable, TwoFactorAuthenticatable;
|
||||||
|
|
||||||
|
public function cart(): HasOne
|
||||||
|
{
|
||||||
|
return $this->hasOne(Cart::class);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the attributes that should be cast.
|
* Get the attributes that should be cast.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use Illuminate\Foundation\Application;
|
|||||||
use Illuminate\Foundation\Configuration\Exceptions;
|
use Illuminate\Foundation\Configuration\Exceptions;
|
||||||
use Illuminate\Foundation\Configuration\Middleware;
|
use Illuminate\Foundation\Configuration\Middleware;
|
||||||
use Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets;
|
use Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets;
|
||||||
|
use Illuminate\Session\Middleware\StartSession;
|
||||||
|
|
||||||
return Application::configure(basePath: dirname(__DIR__))
|
return Application::configure(basePath: dirname(__DIR__))
|
||||||
->withRouting(
|
->withRouting(
|
||||||
@@ -17,6 +18,10 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
->withMiddleware(function (Middleware $middleware): void {
|
->withMiddleware(function (Middleware $middleware): void {
|
||||||
$middleware->encryptCookies(except: ['appearance', 'sidebar_state']);
|
$middleware->encryptCookies(except: ['appearance', 'sidebar_state']);
|
||||||
|
|
||||||
|
$middleware->api(append: [
|
||||||
|
StartSession::class,
|
||||||
|
]);
|
||||||
|
|
||||||
$middleware->web(append: [
|
$middleware->web(append: [
|
||||||
HandleAppearance::class,
|
HandleAppearance::class,
|
||||||
HandleInertiaRequests::class,
|
HandleInertiaRequests::class,
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Cart;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends Factory<Cart>
|
||||||
|
*/
|
||||||
|
class CartFactory extends Factory
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Define the model's default state.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'user_id' => \App\Models\User::factory(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('carts', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('user_id')->constrained();
|
||||||
|
$table->timestamp('stale_date')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('carts');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('cart_product', function (Blueprint $table) {
|
||||||
|
$table->foreignId('cart_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->foreignId('product_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->unsignedInteger('quantity')->default(1);
|
||||||
|
$table->primary(['cart_id', 'product_id']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('cart_product');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('carts', function (Blueprint $table) {
|
||||||
|
$table->unique('user_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('carts', function (Blueprint $table) {
|
||||||
|
$table->dropUnique(['user_id']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('carts', function (Blueprint $table) {
|
||||||
|
$table->dropUnique(['user_id']);
|
||||||
|
$table->foreignId('user_id')->nullable()->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('carts', function (Blueprint $table) {
|
||||||
|
$table->foreignId('user_id')->nullable(false)->change();
|
||||||
|
$table->unique('user_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
import { Head } from '@inertiajs/vue3';
|
import { Head } from '@inertiajs/vue3';
|
||||||
|
import { useHttp } from '@inertiajs/vue3';
|
||||||
|
import { store, update, destroy } from '@/actions/App/Http/Controllers/CartController';
|
||||||
|
|
||||||
interface Product {
|
interface Product {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -9,12 +12,56 @@ interface Product {
|
|||||||
image_url: string | null;
|
image_url: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
defineProps<{
|
interface CartItem {
|
||||||
|
product_id: number;
|
||||||
|
quantity: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
products: Product[];
|
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 {
|
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>
|
</script>
|
||||||
|
|
||||||
@@ -23,11 +70,12 @@ function formatPrice(cents: number): string {
|
|||||||
<link rel="preconnect" href="https://rsms.me/" />
|
<link rel="preconnect" href="https://rsms.me/" />
|
||||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
||||||
</Head>
|
</Head>
|
||||||
<div class="min-h-screen bg-[#FDFDFC] p-6 text-[#1b1b18] dark:bg-[#0a0a0a] dark:text-[#FDFDFC] lg:p-8">
|
<div class="flex min-h-screen bg-[#FDFDFC] text-[#1b1b18] dark:bg-[#0a0a0a] dark:text-[#FDFDFC]">
|
||||||
<header class="mx-auto mb-8 max-w-4xl">
|
<div class="flex-1 p-6 lg:p-8">
|
||||||
|
<header class="mb-8">
|
||||||
<h1 class="text-2xl font-semibold">Packages</h1>
|
<h1 class="text-2xl font-semibold">Packages</h1>
|
||||||
</header>
|
</header>
|
||||||
<main class="mx-auto max-w-4xl">
|
<main>
|
||||||
<p v-if="products.length === 0" class="text-gray-500">No packages available.</p>
|
<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-else class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
<div
|
<div
|
||||||
@@ -39,7 +87,7 @@ function formatPrice(cents: number): string {
|
|||||||
v-if="product.image_url"
|
v-if="product.image_url"
|
||||||
:src="product.image_url"
|
:src="product.image_url"
|
||||||
:alt="product.name"
|
:alt="product.name"
|
||||||
class="h-48 w-full object-cover"
|
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">
|
<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>
|
<span class="text-sm text-gray-400">No image</span>
|
||||||
@@ -49,7 +97,12 @@ function formatPrice(cents: number): string {
|
|||||||
<p class="mb-4 grow text-sm text-gray-500 dark:text-gray-400">{{ product.description }}</p>
|
<p class="mb-4 grow text-sm text-gray-500 dark:text-gray-400">{{ product.description }}</p>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<p class="text-lg font-semibold">{{ formatPrice(product.price_cents) }}</p>
|
<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">
|
<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
|
Add
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -58,4 +111,38 @@ function formatPrice(cents: number): string {
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</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>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
+6
-4
@@ -1,9 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
use App\Http\Controllers\CartController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::get('/cart', function (Request $request) {
|
//middleware('auth:sanctum')->
|
||||||
return $request->user()->cart();
|
Route::group([], function () {
|
||||||
|
Route::post('/cart', [CartController::class, 'store'])->name('cart.store');
|
||||||
|
Route::patch('/cart/{product}', [CartController::class, 'update'])->name('cart.update');
|
||||||
|
Route::delete('/cart/{product}', [CartController::class, 'destroy'])->name('cart.destroy');
|
||||||
});
|
});
|
||||||
//})->middleware('auth:sanctum');
|
|
||||||
|
|||||||
+14
-2
@@ -1,5 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Cart;
|
||||||
use App\Models\Product;
|
use App\Models\Product;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use Laravel\Fortify\Features;
|
use Laravel\Fortify\Features;
|
||||||
@@ -8,9 +9,20 @@ use Laravel\Fortify\Features;
|
|||||||
'canRegister' => Features::enabled(Features::registration()),
|
'canRegister' => Features::enabled(Features::registration()),
|
||||||
])->name('home');*/
|
])->name('home');*/
|
||||||
|
|
||||||
Route::get('/', fn () => inertia('Home', [
|
Route::get('/', function () {
|
||||||
|
$cartId = session('cart_id');
|
||||||
|
$cart = $cartId ? Cart::with('products')->find($cartId) : null;
|
||||||
|
|
||||||
|
return inertia('Home', [
|
||||||
'products' => Product::all(),
|
'products' => Product::all(),
|
||||||
]))->name('home');
|
'cart' => $cart
|
||||||
|
? $cart->products->map(fn ($p) => [
|
||||||
|
'product_id' => $p->id,
|
||||||
|
'quantity' => $p->pivot->quantity,
|
||||||
|
])
|
||||||
|
: [],
|
||||||
|
]);
|
||||||
|
})->name('home');
|
||||||
|
|
||||||
Route::middleware(['auth', 'verified'])->group(function () {
|
Route::middleware(['auth', 'verified'])->group(function () {
|
||||||
Route::inertia('dashboard', 'Dashboard')->name('dashboard');
|
Route::inertia('dashboard', 'Dashboard')->name('dashboard');
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Cart;
|
||||||
|
use App\Models\Product;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
test('guests can add to cart without being authenticated', function () {
|
||||||
|
$product = Product::factory()->create();
|
||||||
|
|
||||||
|
$this->postJson('/api/cart', ['product_id' => $product->id])
|
||||||
|
->assertSuccessful()
|
||||||
|
->assertJsonFragment(['product_id' => $product->id, 'quantity' => 1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('adding a product creates a cart and returns cart items', function () {
|
||||||
|
$product = Product::factory()->create();
|
||||||
|
|
||||||
|
$response = $this->postJson('/api/cart', ['product_id' => $product->id]);
|
||||||
|
|
||||||
|
$response->assertSuccessful()
|
||||||
|
->assertJsonFragment(['product_id' => $product->id, 'quantity' => 1]);
|
||||||
|
|
||||||
|
expect(Cart::count())->toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('adding the same product increments quantity', function () {
|
||||||
|
$product = Product::factory()->create();
|
||||||
|
|
||||||
|
$this->postJson('/api/cart', ['product_id' => $product->id]);
|
||||||
|
$response = $this->postJson('/api/cart', ['product_id' => $product->id]);
|
||||||
|
|
||||||
|
$response->assertSuccessful()
|
||||||
|
->assertJsonFragment(['product_id' => $product->id, 'quantity' => 2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('adding a non-existent product fails validation', function () {
|
||||||
|
$this->postJson('/api/cart', ['product_id' => 9999])
|
||||||
|
->assertUnprocessable();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('updating a cart item changes its quantity', function () {
|
||||||
|
$product = Product::factory()->create();
|
||||||
|
$cart = Cart::factory()->create(['user_id' => null]);
|
||||||
|
$cart->products()->attach($product->id, ['quantity' => 1]);
|
||||||
|
|
||||||
|
$response = $this->withSession(['cart_id' => $cart->id])
|
||||||
|
->patchJson("/api/cart/{$product->id}", ['quantity' => 5]);
|
||||||
|
|
||||||
|
$response->assertSuccessful()
|
||||||
|
->assertJsonFragment(['product_id' => $product->id, 'quantity' => 5]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('removing a product detaches it from the cart', function () {
|
||||||
|
$product = Product::factory()->create();
|
||||||
|
$cart = Cart::factory()->create(['user_id' => null]);
|
||||||
|
$cart->products()->attach($product->id, ['quantity' => 2]);
|
||||||
|
|
||||||
|
$response = $this->withSession(['cart_id' => $cart->id])
|
||||||
|
->deleteJson("/api/cart/{$product->id}");
|
||||||
|
|
||||||
|
$response->assertSuccessful()
|
||||||
|
->assertJsonMissing(['product_id' => $product->id]);
|
||||||
|
|
||||||
|
expect($cart->fresh()->products)->toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a cart can optionally belong to a user', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$cartWithUser = Cart::factory()->create(['user_id' => $user->id]);
|
||||||
|
$cartGuest = Cart::factory()->create(['user_id' => null]);
|
||||||
|
|
||||||
|
expect($cartWithUser->user)->toBeInstanceOf(User::class);
|
||||||
|
expect($cartGuest->user)->toBeNull();
|
||||||
|
});
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Cart;
|
||||||
|
use App\Models\Product;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
test('a cart belongs to a user when one is set', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$cart = Cart::factory()->create(['user_id' => $user->id]);
|
||||||
|
|
||||||
|
expect($cart->user->id)->toBe($user->id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a cart can exist without a user', function () {
|
||||||
|
$cart = Cart::factory()->create(['user_id' => null]);
|
||||||
|
|
||||||
|
expect($cart->user)->toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a user has one cart', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
Cart::factory()->create(['user_id' => $user->id]);
|
||||||
|
|
||||||
|
expect($user->cart)->toBeInstanceOf(Cart::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a cart can have products with quantities', function () {
|
||||||
|
$cart = Cart::factory()->create(['user_id' => null]);
|
||||||
|
$product = Product::factory()->create();
|
||||||
|
|
||||||
|
$cart->products()->attach($product->id, ['quantity' => 3]);
|
||||||
|
|
||||||
|
expect($cart->products)->toHaveCount(1);
|
||||||
|
expect($cart->products->first()->pivot->quantity)->toBe(3);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user