Compare commits

..

4 Commits

Author SHA1 Message Date
Bayu Lukman Yusuf 9430b4895e Merge branch 'feature-product' into development
WMS API/ECOMMERCE/pipeline/head This commit looks good Details
2026-02-27 16:30:28 +07:00
Bayu Lukman Yusuf f78819c9b9 search 2026-02-27 16:29:57 +07:00
Bayu Lukman Yusuf f2c2cf11ed wishlist 2026-02-27 16:19:51 +07:00
Bayu Lukman Yusuf 8ef3a2783a discount - use point 2026-02-27 11:57:16 +07:00
23 changed files with 1186 additions and 377 deletions

View File

@ -35,7 +35,7 @@ class CartController extends Controller
public function add(MemberCartRequest $request, MemberCartRepository $repository)
{
Log::info($request->all());
// Log::info($request->all());
$data = $request->validated();
$item = $repository->create($data);

View File

@ -9,7 +9,6 @@ use App\Repositories\Member\ShippingRepository;
use App\Repositories\Member\Transaction\TransactionRepository;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\ValidationException;
class CheckoutController extends Controller
@ -22,16 +21,12 @@ class CheckoutController extends Controller
$subtotal = $memberCartRepository->getSubtotal($request->input('location_id'));
$address_list = Address::where('user_id', auth()->user()->id)->orderBy('is_primary','desc')->get();
$address_list = Address::where('user_id', auth()->user()->id)->orderBy('is_primary', 'desc')->get();
$total = $subtotal;
$carts = $memberCartRepository->getList($request);
return view('checkout.v1-delivery-1', [
'carts' => $carts,
'subtotal' => $subtotal,
@ -46,9 +41,8 @@ class CheckoutController extends Controller
$delivery_method = $request->input('delivery_method') ?? 'shipping';
$address_id = $request->input('address_id');
if ($address_id == null) {
$address_list = Address::where('user_id', $request->user()->id)->orderBy('is_primary','desc')->get();
$address_list = Address::where('user_id', $request->user()->id)->orderBy('is_primary', 'desc')->get();
$address_id = $address_list->first()->id;
}
@ -60,19 +54,20 @@ class CheckoutController extends Controller
if ($delivery_method == 'shipping') {
session(['checkout_delivery_method' => $delivery_method]);
session(['checkout_address_id' => $address_id]);
return redirect()->route('checkout.shipping');
}
if ($delivery_method == 'pickup') {
session(['checkout_delivery_method' => $delivery_method]);
session(['checkout_address_id' => null]);
return redirect()->route('checkout.payment');
}
return redirect()->back()->with('error', 'Delivery method is not valid');
}
public function chooseShipping(Request $request, MemberCartRepository $memberCartRepository, ShippingRepository $shippingRepository)
{
try {
@ -81,7 +76,6 @@ class CheckoutController extends Controller
$location_id = session('location_id', 22);
if ($delivery_method == null || $address_id == null || $location_id == null) {
return redirect()->route('checkout.delivery');
@ -90,21 +84,19 @@ class CheckoutController extends Controller
$subtotal = $memberCartRepository->getSubtotal($location_id);
$total = $subtotal;
$request->merge(['location_id' => $location_id]);
$carts = $memberCartRepository->getList($request);
try{
try {
$shipping_list = collect($shippingRepository->getList([
"location_id" => $location_id,
"address_id" => $address_id,
"items" => $carts,
])['pricing'] ?? [])->map(function($row){
'location_id' => $location_id,
'address_id' => $address_id,
'items' => $carts,
])['pricing'] ?? [])->map(function ($row) {
return [
'courier' => $row['courier_code'],
'service' => $row['courier_service_code'],
'title' => $row['courier_name']." - ".$row['courier_service_name'],
'title' => $row['courier_name'].' - '.$row['courier_service_name'],
'description' => $row['duration'],
'cost' => $row['shipping_fee'],
];
@ -112,12 +104,20 @@ class CheckoutController extends Controller
if (count($shipping_list) == 0) {
throw ValidationException::withMessages([
"message" => "Tidak dapat menghitung ongkir"
'message' => 'Tidak dapat menghitung ongkir',
]);
}
}catch(\Exception $e){
} catch (\Exception $e) {
$message = $e->getMessage();
// if contain Failed due to invalid or missing postal code
if (str_contains($message, 'Failed due to invalid or missing postal code')) {
$message = 'Kode pos tidak valid atau tidak ditemukan';
} else if (str_contains($message, 'support@biteship.com')) {
$message = 'Layanan pengiriman tidak tersedia untuk alamat ini';
}
throw ValidationException::withMessages([
"message" => $e->getMessage()
'message' => $message,
]);
}
@ -132,6 +132,7 @@ class CheckoutController extends Controller
]);
} catch (\Exception $e) {
Log::info($e);
return redirect()->route('checkout.delivery')->with('error', $e->getMessage() ?? 'Invalid checkout data');
}
}
@ -164,6 +165,8 @@ class CheckoutController extends Controller
$location_id = session('location_id', 22);
$use_point = session('use_point') ?? 0;
$items = [];
$request->merge(['location_id' => $location_id]);
@ -175,22 +178,24 @@ class CheckoutController extends Controller
foreach ($carts as $cart) {
$items[] = [
"item_reference_id" => $cart->item_reference_id,
"qty" => $cart->qty
'item_reference_id' => $cart->item_reference_id,
'qty' => $cart->qty,
];
}
$data = [
"address_id" => $address_id,
"note" => "",
"courier_company" => $courier,
"courier_type" => $service,
"location_id" => $location_id,
"items" => $items,
"vouchers" => [],
"use_customer_points" => 0,
'address_id' => $address_id,
'note' => '',
'courier_company' => $courier,
'courier_type' => $service,
'location_id' => $location_id,
'items' => $items,
'vouchers' => [],
'use_customer_points' => $use_point ?? 0,
];
dd($data);
$item = $repository->create($data);
$notification = new \App\Notifications\Member\Transaction\OrderWaitPayment($item);
@ -201,20 +206,21 @@ class CheckoutController extends Controller
// proses payment
$payment = $item->payments()->where("method_type",'App\Models\XenditLink')
->where("status",'PENDING')
$payment = $item->payments()->where('method_type', 'App\Models\XenditLink')
->where('status', 'PENDING')
->first();
$invoice_url = $payment ? @$payment->method->invoice_url: "";
$invoice_url = $payment ? @$payment->method->invoice_url : '';
// reset state session
session()->forget(['checkout_delivery_method', 'checkout_address_id', 'checkout_courier', 'checkout_service', 'use_point']);
return redirect()->to($invoice_url)->withHeaders([
'Cache-Control' => 'no-cache, no-store, must-revalidate',
'Pragma' => 'no-cache',
'Expires' => '0'
'Expires' => '0',
]);
} catch (\Exception $e) {
Log::info($e);
@ -222,4 +228,92 @@ class CheckoutController extends Controller
return redirect()->route('checkout.delivery')->with('error', 'Invalid checkout data');
}
}
/**
* Apply points to session
*/
public function applyPoint(Request $request)
{
try {
$usePoint = $request->input('use_point');
// Validate input
if (!$usePoint || !is_numeric($usePoint) || $usePoint < 0) {
return response()->json([
'success' => false,
'message' => 'Invalid points value'
]);
}
$usePoint = (int) $usePoint;
// Get user's available points
$userPoints = auth()->user()->customer->point ?? 0;
// Check if user has enough points
if ($usePoint > $userPoints) {
return response()->json([
'success' => false,
'message' => 'You cannot use more points than available'
]);
}
// Set points in session
session(['use_point' => $usePoint]);
return response()->json([
'success' => true,
'message' => 'Points applied successfully',
'points_applied' => $usePoint,
'remaining_points' => $userPoints - $usePoint
]);
} catch (\Exception $e) {
Log::error('Error applying points: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'An error occurred while applying points'
]);
}
}
/**
* Remove points from session
*/
public function removePoint(Request $request)
{
try {
// Check if points are currently applied
$currentPoints = session('use_point', 0);
if ($currentPoints <= 0) {
return response()->json([
'success' => false,
'message' => 'No points are currently applied'
]);
}
// Remove points from session
session()->forget('use_point');
// Get user's available points
$userPoints = auth()->user()->customer->point ?? 0;
return response()->json([
'success' => true,
'message' => 'Points removed successfully',
'points_removed' => $currentPoints,
'available_points' => $userPoints
]);
} catch (\Exception $e) {
Log::error('Error removing points: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'An error occurred while removing points'
]);
}
}
}

View File

@ -39,7 +39,7 @@ class VoucherEventController extends Controller
// Call the repository's redeem method
$result = $this->voucherEventRepository->redeem($voucherEvent, $user);
if ($result['success']) {
if ($result) {
return response()->json([
'success' => true,
'message' => $result['message'] ?? 'Voucher redeemed successfully!',

View File

@ -0,0 +1,84 @@
<?php
namespace App\Http\Controllers;
use App\Repositories\Member\WishlistRepository;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Auth;
class WishController extends Controller
{
protected $wishlistRepository;
public function __construct(WishlistRepository $wishlistRepository)
{
$this->wishlistRepository = $wishlistRepository;
}
/**
* Display the wishlist page
*/
public function index(Request $request)
{
try {
$wishlists = $this->wishlistRepository->getList($request);
return view('account.wishlist', [
'wishlists' => $wishlists,
'user' => Auth::user()
]);
} catch (\Exception $e) {
return redirect()->back()->with('error', 'Failed to load wishlist: ' . $e->getMessage());
}
}
/**
* Add item to wishlist
*/
public function store(Request $request): JsonResponse
{
$request->validate([
'item_id' => 'required|exists:items,id'
]);
try {
$wishlist = $this->wishlistRepository->create([
'item_id' => $request->item_id
]);
return response()->json([
'success' => true,
'message' => 'Item added to wishlist successfully',
'wishlist' => $wishlist
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Failed to add item to wishlist: ' . $e->getMessage()
], 500);
}
}
/**
* Remove item from wishlist
*/
public function destroy(Request $request, $id): JsonResponse
{
try {
$this->wishlistRepository->delete($id);
return response()->json([
'success' => true,
'message' => 'Item removed from wishlist successfully'
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Failed to remove item from wishlist: ' . $e->getMessage()
], 500);
}
}
}

View File

@ -199,4 +199,6 @@ class ItemReference extends Model
return $result;
}
}

View File

@ -355,4 +355,10 @@ class Items extends Model
return 0;
}
}
public function isWishlist() : bool
{
return $this->hasOne(Wishlist::class, 'item_id', 'id')->where('customer_id', auth()->user()->customer->id)->exists();
}
}

27
app/Models/Wishlist.php Normal file
View File

@ -0,0 +1,27 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Wishlist extends Model
{
use HasFactory;
protected $fillable = [
'customer_id',
'item_id',
];
public function customer(): BelongsTo
{
return $this->belongsTo(Customer::class);
}
public function item(): BelongsTo
{
return $this->belongsTo(Items::class);
}
}

View File

@ -0,0 +1,75 @@
<?php
namespace App\Repositories\Member;
use App\Models\Wishlist;
use App\Models\Customer;
use Illuminate\Support\Facades\DB;
class WishlistRepository
{
public function getList($request)
{
$customer = Customer::where('user_id', auth()->user()->id)->first();
if (!$customer) {
throw new \Exception('Customer not found');
}
$limit = 20;
$wishlist = Wishlist::where('customer_id', $customer->id)
->with([
'item.variants',
])
->orderBy('created_at', 'desc')
->paginate($limit);
return $wishlist;
}
public function create($data)
{
$model = DB::transaction(function () use ($data) {
$customer = Customer::where('user_id', auth()->user()->id)->first();
// Check if item already exists in wishlist
$existing = Wishlist::where('customer_id', $customer->id)
->where('item_id', $data['item_id'])
->first();
if ($existing) {
return $existing; // Return existing item if already in wishlist
}
$model = Wishlist::create([
'customer_id' => $customer->id,
'item_id' => $data['item_id'],
]);
return $model;
});
return $model;
}
public function delete($item_id)
{
$wishlist = DB::transaction(function () use ($item_id) {
$wishlist = Wishlist::where('customer_id', auth()->user()->customer->id)
->where('item_id', $item_id)
->first();
if (!$wishlist) {
throw new \Exception('Wishlist not found');
}
$wishlist->delete();
return $wishlist;
});
return $wishlist;
}
}

View File

@ -56,4 +56,16 @@ return [
'terms_and_conditions' => 'Terms and Conditions',
'close' => 'Close',
'redeem' => 'Redeem',
'use_point' => 'Use Points',
'your_points' => 'Your Points',
'available_points' => 'Available Points',
'input_point' => 'Enter points to use',
'apply_point' => 'Apply Points',
'remove_point' => 'Remove Points',
'points_applied' => 'Points Applied',
'points_used' => 'points used',
'saving' => 'Discount:',
'shipping' => 'Shipping:',
'estimated_total' => 'Estimated total:',
'proceed_to_checkout' => 'Proceed to checkout',
];

View File

@ -50,10 +50,22 @@ return [
'enter_valid_promo_code' => 'Masukkan kode promo yang valid!',
'apply' => 'Gunakan',
'available_vouchers' => 'Voucher Tersedia',
'available_voucher_events' => 'Event Voucher Tersedia',
'available_voucher_events' => 'Tukar Poin',
'points_required' => 'Poin Diperlukan',
'valid_period' => 'Periode Berlaku',
'terms_and_conditions' => 'Syarat dan Ketentuan',
'close' => 'Tutup',
'redeem' => 'Tukar',
'use_point' => 'Gunakan Poin',
'your_points' => 'Poin Anda',
'available_points' => 'Poin Tersedia',
'input_point' => 'Masukkan poin yang akan digunakan',
'apply_point' => 'Gunakan Poin',
'remove_point' => 'Hapus Poin',
'points_applied' => 'Poin Diterapkan',
'points_used' => 'poin digunakan',
'saving' => 'Diskon:',
'shipping' => 'Pengiriman:',
'estimated_total' => 'Total estimasi:',
'proceed_to_checkout' => 'Lanjut ke checkout',
];

View File

@ -7,15 +7,15 @@
<!-- Page title + Add list button-->
<div class="d-flex align-items-center justify-content-between pb-3 mb-1 mb-sm-2 mb-md-3">
<h1 class="h2 me-3 mb-0">Wishlist</h1>
<div class="nav">
{{-- <div class="nav">
<a class="nav-link animate-underline px-0 py-1 py-ms-2" data-bs-toggle="modal" href="#wishlistModal">
<i class="ci-plus fs-base me-1"></i>
<span class="animate-target">Add wishlist</span>
</a>
</div>
</div> --}}
</div>
<!-- Wishlist selector -->
<div class="border-bottom pb-4 mb-3">
{{-- <div class="border-bottom pb-4 mb-3">
<div class="row align-items-center justify-content-between">
<div class="col-sm-7 col-md-8 col-xxl-9 d-flex align-items-center mb-3 mb-sm-0">
<h5 class="me-2 mb-0">Interesting offers</h5>
@ -69,9 +69,9 @@
</select>
</div>
</div>
</div>
</div> --}}
<!-- Master checkbox + Action buttons -->
<div class="nav align-items-center mb-4">
{{-- <div class="nav align-items-center mb-4">
<div class="form-checkl nav-link animate-underline fs-lg ps-0 pe-2 py-2 mt-n1 me-4"
data-master-checkbox='{"container": "#wishlistSelection", "label": "Select all", "labelChecked": "Unselect all", "showOnCheck": "#action-buttons"}'>
<input checked="" class="form-check-input" id="wishlist-master" type="checkbox" />
@ -92,232 +92,14 @@
<span class="animate-target d-none d-md-inline">Remove selected</span>
</a>
</div>
</div>
</div> --}}
<!-- Wishlist items (Grid) -->
<div class="row row-cols-2 row-cols-md-3 g-4" id="wishlistSelection">
<!-- Item -->
<div class="col">
<div class="product-card animate-underline hover-effect-opacity bg-body rounded">
<div class="position-relative">
<div class="position-absolute top-0 end-0 z-1 pt-1 pe-1 mt-2 me-2">
<div class="form-check fs-lg">
<input checked="" class="form-check-input select-card-check" type="checkbox" />
</div>
</div>
<a class="d-block rounded-top overflow-hidden p-3 p-sm-4"
href="{{ route('second', ['shop', 'product-general-electronics']) }}">
<span
class="badge bg-danger position-absolute top-0 start-0 mt-2 ms-2 mt-lg-3 ms-lg-3">-21%</span>
<div class="ratio" style="--cz-aspect-ratio: calc(240 / 258 * 100%)">
<img alt="VR Glasses" src="/img/shop/electronics/01.png" />
</div>
</a>
</div>
<div class="w-100 min-w-0 px-1 pb-2 px-sm-3 pb-sm-3">
<div class="d-flex align-items-center gap-2 mb-2">
<div class="d-flex gap-1 fs-xs">
<i class="ci-star-filled text-warning"></i>
<i class="ci-star-filled text-warning"></i>
<i class="ci-star-filled text-warning"></i>
<i class="ci-star-filled text-warning"></i>
<i class="ci-star text-body-tertiary opacity-75"></i>
</div>
<span class="text-body-tertiary fs-xs">(123)</span>
</div>
<h3 class="pb-1 mb-2">
<a class="d-block fs-sm fw-medium text-truncate"
href="{{ route('second', ['shop', 'product-general-electronics']) }}">
<span class="animate-target">VRB01 Virtual Reality Glasses</span>
</a>
</h3>
<div class="d-flex align-items-center justify-content-between">
<div class="h5 lh-1 mb-0">$340.99 <del
class="text-body-tertiary fs-sm fw-normal">$430.00</del></div>
<button aria-label="Add to Cart"
class="product-card-button btn btn-icon btn-secondary animate-slide-end ms-2"
type="button">
<i class="ci-shopping-cart fs-base animate-target"></i>
</button>
</div>
</div>
</div>
</div>
<!-- Item -->
<div class="col">
<div class="product-card animate-underline hover-effect-opacity bg-body rounded">
<div class="position-relative">
<div class="position-absolute top-0 end-0 z-1 pt-1 pe-1 mt-2 me-2">
<div class="form-check fs-lg">
<input checked="" class="form-check-input select-card-check" type="checkbox" />
</div>
</div>
<a class="d-block rounded-top overflow-hidden p-3 p-sm-4"
href="{{ route('second', ['shop', 'product-general-electronics']) }}">
<div class="ratio" style="--cz-aspect-ratio: calc(240 / 258 * 100%)">
<img alt="iPhone 14" src="/img/shop/electronics/02.png" />
</div>
</a>
</div>
<div class="w-100 min-w-0 px-1 pb-2 px-sm-3 pb-sm-3">
<div class="d-flex align-items-center gap-2 mb-2">
<div class="d-flex gap-1 fs-xs">
<i class="ci-star-filled text-warning"></i>
<i class="ci-star-filled text-warning"></i>
<i class="ci-star-filled text-warning"></i>
<i class="ci-star-filled text-warning"></i>
<i class="ci-star-half text-warning"></i>
</div>
<span class="text-body-tertiary fs-xs">(142)</span>
</div>
<h3 class="pb-1 mb-2">
<a class="d-block fs-sm fw-medium text-truncate"
href="{{ route('second', ['shop', 'product-general-electronics']) }}">
<span class="animate-target">Apple iPhone 14 128GB White</span>
</a>
</h3>
<div class="d-flex align-items-center justify-content-between">
<div class="h5 lh-1 mb-0">$899.00</div>
<button aria-label="Add to Cart"
class="product-card-button btn btn-icon btn-secondary animate-slide-end ms-2"
type="button">
<i class="ci-shopping-cart fs-base animate-target"></i>
</button>
</div>
</div>
</div>
</div>
<!-- Item -->
<div class="col">
<div class="product-card animate-underline hover-effect-opacity bg-body rounded">
<div class="position-relative">
<div class="position-absolute top-0 end-0 z-1 pt-1 pe-1 mt-2 me-2">
<div class="form-check fs-lg">
<input class="form-check-input select-card-check" type="checkbox" />
</div>
</div>
<a class="d-block rounded-top overflow-hidden p-3 p-sm-4"
href="{{ route('second', ['shop', 'product-general-electronics']) }}">
<div class="ratio" style="--cz-aspect-ratio: calc(240 / 258 * 100%)">
<img alt="Smart Watch" src="/img/shop/electronics/03.png" />
</div>
</a>
</div>
<div class="w-100 min-w-0 px-1 pb-2 px-sm-3 pb-sm-3">
<div class="d-flex align-items-center gap-2 mb-2">
<div class="d-flex gap-1 fs-xs">
<i class="ci-star-filled text-warning"></i>
<i class="ci-star-filled text-warning"></i>
<i class="ci-star-filled text-warning"></i>
<i class="ci-star-filled text-warning"></i>
<i class="ci-star-filled text-warning"></i>
</div>
<span class="text-body-tertiary fs-xs">(67)</span>
</div>
<h3 class="pb-1 mb-2">
<a class="d-block fs-sm fw-medium text-truncate"
href="{{ route('second', ['shop', 'product-general-electronics']) }}">
<span class="animate-target">Smart Watch Series 7, White</span>
</a>
</h3>
<div class="d-flex align-items-center justify-content-between">
<div class="h5 lh-1 mb-0">$429.00</div>
<button aria-label="Add to Cart"
class="product-card-button btn btn-icon btn-secondary animate-slide-end ms-2"
type="button">
<i class="ci-shopping-cart fs-base animate-target"></i>
</button>
</div>
</div>
</div>
</div>
<!-- Item -->
<div class="col">
<div class="product-card animate-underline hover-effect-opacity bg-body rounded">
<div class="posittion-relative">
<div class="position-absolute top-0 end-0 z-1 pt-1 pe-1 mt-2 me-2">
<div class="form-check fs-lg">
<input class="form-check-input select-card-check" type="checkbox" />
</div>
</div>
<a class="d-block rounded-top overflow-hidden p-3 p-sm-4"
href="{{ route('second', ['shop', 'product-general-electronics']) }}">
<div class="ratio" style="--cz-aspect-ratio: calc(240 / 258 * 100%)">
<img alt="iPad Air" src="/img/shop/electronics/05.png" />
</div>
</a>
</div>
<div class="w-100 min-w-0 px-1 pb-2 px-sm-3 pb-sm-3">
<div class="d-flex align-items-center gap-2 mb-2">
<div class="d-flex gap-1 fs-xs">
<i class="ci-star-filled text-warning"></i>
<i class="ci-star-filled text-warning"></i>
<i class="ci-star-filled text-warning"></i>
<i class="ci-star-filled text-warning"></i>
<i class="ci-star-filled text-warning"></i>
</div>
<span class="text-body-tertiary fs-xs">(12)</span>
</div>
<h3 class="pb-1 mb-2">
<a class="d-block fs-sm fw-medium text-truncate"
href="{{ route('second', ['shop', 'product-general-electronics']) }}">
<span class="animate-target">Tablet Apple iPad Air M1</span>
</a>
</h3>
<div class="d-flex align-items-center justify-content-between">
<div class="h5 lh-1 mb-0">$540.00</div>
<button aria-label="Add to Cart"
class="product-card-button btn btn-icon btn-secondary animate-slide-end ms-2"
type="button">
<i class="ci-shopping-cart fs-base animate-target"></i>
</button>
</div>
</div>
</div>
</div>
<!-- Item -->
<div class="col">
<div class="product-card animate-underline hover-effect-opacity bg-body rounded">
<div class="position-relative">
<div class="position-absolute top-0 end-0 z-1 pt-1 pe-1 mt-2 me-2">
<div class="form-check fs-lg">
<input class="form-check-input select-card-check" type="checkbox" />
</div>
</div>
<a class="d-block rounded-top overflow-hidden p-3 p-sm-4"
href="{{ route('second', ['shop', 'product-general-electronics']) }}">
<div class="ratio" style="--cz-aspect-ratio: calc(240 / 258 * 100%)">
<img alt="AirPods 2" src="/img/shop/electronics/06.png" />
</div>
</a>
</div>
<div class="w-100 min-w-0 px-1 pb-2 px-sm-3 pb-sm-3">
<div class="d-flex align-items-center gap-2 mb-2">
<div class="d-flex gap-1 fs-xs">
<i class="ci-star-filled text-warning"></i>
<i class="ci-star-filled text-warning"></i>
<i class="ci-star-filled text-warning"></i>
<i class="ci-star-filled text-warning"></i>
<i class="ci-star text-body-tertiary opacity-75"></i>
</div>
<span class="text-body-tertiary fs-xs">(78)</span>
</div>
<h3 class="pb-1 mb-2">
<a class="d-block fs-sm fw-medium text-truncate"
href="{{ route('second', ['shop', 'product-general-electronics']) }}">
<span class="animate-target">Headphones Apple AirPods 2 Pro</span>
</a>
</h3>
<div class="d-flex align-items-center justify-content-between">
<div class="h5 lh-1 mb-0">$224.00</div>
<button aria-label="Add to Cart"
class="product-card-button btn btn-icon btn-secondary animate-slide-end ms-2"
type="button">
<i class="ci-shopping-cart fs-base animate-target"></i>
</button>
</div>
</div>
</div>
</div>
@foreach ($wishlists as $key => $value)
@include('components.home.product-card', ['product' => $value->item])
@endforeach
</div>
</div>
</div>

View File

@ -186,26 +186,26 @@
<span class="text-dark-emphasis fw-medium" id="cart-subtotal">Rp 0</span>
</li>
<li class="d-flex justify-content-between">
Saving:
<span class="text-danger fw-medium">Rp 0</span>
{{ __('checkout.saving') }}
<span class="text-danger fw-medium">Rp {{ number_format(session('use_point') ?? 0,0,",",".") }}</span>
</li>
{{-- <li class="d-flex justify-content-between" id="tax-row" style="display: none;">
Tax collected:
<span class="text-dark-emphasis fw-medium">Rp 0</span>
</li> --}}
<li class="d-flex justify-content-between">
Shipping:
{{ __('checkout.shipping') }}
<span class="text-dark-emphasis fw-medium">Calculated at checkout</span>
</li>
</ul>
<div class="border-top pt-4 mt-4">
<div class="d-flex justify-content-between mb-3">
<span class="fs-sm">Estimated total:</span>
<span class="fs-sm">{{ __('checkout.estimated_total') }}</span>
<span class="h5 mb-0" id="cart-estimated-total">$0.00</span>
</div>
<a class="btn btn-lg btn-primary w-100"
href="{{ route('checkout.delivery') }}">
Proceed to checkout
{{ __('checkout.proceed_to_checkout') }}
<i class="ci-chevron-right fs-lg ms-1 me-n1"></i>
</a>
{{-- <div class="nav justify-content-center fs-sm mt-3">
@ -217,7 +217,8 @@
</div>
</div>
</div>
<x-checkout.promo-code />
{{-- <x-checkout.promo-code /> --}}
<x-checkout.use-point />
</div>
</aside>
@endif
@ -1039,7 +1040,7 @@
}
// Calculate estimated total (subtotal - savings + tax)
const savings = 0; // Fixed savings amount
const savings = {{ session('use_point') ?? 0 }}; // Fixed savings amount
const tax = 0; // Fixed tax amount
const estimatedTotal = subtotal - savings + tax;

View File

@ -84,7 +84,7 @@
<!-- Order summary (sticky sidebar) -->
<aside class="col-lg-4 offset-xl-1" style="margin-top: -100px">
<div class="position-sticky top-0" style="padding-top: 100px">
<x-checkout.order-summary :subtotal="$subtotal" :total="$total" :savings="0" :tax="0"
<x-checkout.order-summary :subtotal="$subtotal" :total="$total"
:showEdit="true" :editUrl="route('cart.index')" />
</div>
</aside>
@ -113,6 +113,7 @@
});
function updateOrderSummaryWithShippingCost(shippingValue) {
const saving = {{ session('use_point') ?? 0 }};
console.log('updateOrderSummaryWithShippingCost called with:', shippingValue);
// Parse the shipping value: courier|service|cost
@ -138,7 +139,7 @@
const currentSubtotal = parseFloat(subtotalElement.textContent.replace(/[^\d]/g, ''));
console.log('Current subtotal:', currentSubtotal);
const newTotal = currentSubtotal + cost;
const newTotal = currentSubtotal + cost - saving;
console.log('New total:', newTotal);
// Store original total

View File

@ -191,7 +191,7 @@
<!-- Order summary (sticky sidebar) -->
<aside class="col-lg-4 offset-xl-1" style="margin-top: -100px">
<div class="position-sticky top-0" style="padding-top: 100px">
<x-checkout.order-summary :subtotal="$subtotal" :total="$total" :savings="0" :tax="0"
<x-checkout.order-summary :subtotal="$subtotal" :total="$total"
:showEdit="true" :editUrl="route('cart.index')" />
</div>
</aside>
@ -428,11 +428,12 @@
function updateOrderSummaryWithShipping() {
// Simulate shipping cost calculation based on postcode
const shippingCost = calculateShippingCost(document.getElementById('zip').value);
const saving = {{ session('use_point') ?? 0 }};
// Update order summary
const subtotalElement = document.getElementById('cart-subtotal');
const currentSubtotal = parseFloat(subtotalElement.textContent.replace(/[^\d]/g, ''));
const newTotal = currentSubtotal + shippingCost;
const newTotal = currentSubtotal + 0 - saving;
// Store original total
if (!window.originalTotal) {
@ -481,40 +482,15 @@
}
function showShippingCost(cost, isFree = false) {
// Find or create shipping cost element in order summary
let shippingElement = document.querySelector('[data-shipping-cost]');
if (!shippingElement) {
const orderSummary = document.querySelector('.list-unstyled');
const shippingLi = document.createElement('li');
shippingLi.className = 'd-flex justify-content-between';
shippingLi.setAttribute('data-shipping-cost', '');
shippingLi.innerHTML = `
<span>{{ __('checkout.shipping') }}:</span>
<span class="text-dark-emphasis fw-medium" id="shipping-cost">Rp 0</span>
`;
orderSummary.appendChild(shippingLi);
shippingElement = shippingLi;
}
const costElement = document.getElementById('shipping-cost');
if (isFree) {
costElement.textContent = '{{ __('checkout.free') }}';
costElement.className = 'text-success fw-medium';
} else {
costElement.textContent = `Rp ${number_format(cost, 0, ',', '.')}`;
costElement.className = 'text-dark-emphasis fw-medium';
}
shippingElement.style.display = 'flex';
}
function validateDeliveryForm() {
const postcode = document.getElementById('zip').value;
const firstName = document.getElementById('firstName').value;
const address = document.getElementById('address').value;
const city = document.getElementById('city').value;
const phone = document.getElementById('phone').value;
const postcode = document.getElementById('zip').textContent;
const firstName = document.getElementById('firstName').textContent;
const address = document.getElementById('address').textContent;
const city = document.getElementById('city').textContent;
const phone = document.getElementById('phone').textContent;
if (!postcode) {
alert('{{ __('checkout.enter_postcode') }}');

View File

@ -1,7 +1,7 @@
@props([
'subtotal' => 0,
'total' => 0,
'savings' => 0,
'savings' => session('use_point'),
'tax' => 0,
'showEdit' => false,
'editUrl' => null,
@ -36,9 +36,7 @@
</div>
<ul class="list-unstyled fs-sm gap-3 mb-0">
<li class="d-flex justify-content-between">
<div>{{ __('cart_summary.subtotal') }} (
{{ __('cart_summary.items_count', ['count' => auth()->check() ? \App\Repositories\Member\Cart\MemberCartRepository::getCount() : 0]) }}):
</div>
<div>{{ __('cart_summary.subtotal') }} ({{ __('cart_summary.items_count', ['count' => auth()->check() ? \App\Repositories\Member\Cart\MemberCartRepository::getCount() : 0]) }})</div>
<span class="text-dark-emphasis fw-medium" id="cart-subtotal">Rp
{{ number_format($subtotal, 0, ',', '.') }}</span>
</li>

View File

@ -48,8 +48,13 @@
<div class="d-flex flex-wrap gap-2">
@foreach($vouchers as $voucher)
<span class="badge bg-light text-dark px-3 py-2">
{{ $voucher->code }}
<span class="badge bg-light text-dark px-3 py-2"
style="cursor: pointer; transition: all 0.2s ease;"
onclick="applyVoucher('{{ $voucher->code }}')"
onmouseover="this.style.backgroundColor='#e9ecef'; this.style.transform='scale(1.05)';"
onmouseout="this.style.backgroundColor='#f8f9fa'; this.style.transform='scale(1)';"
title="Click to apply {{ $voucher->code }}">
{{ $voucher->description }}
</span>
@endforeach
</div>
@ -187,6 +192,84 @@ document.addEventListener('DOMContentLoaded', function () {
});
// Handle promo code form submission
document.addEventListener('submit', function(e) {
const form = e.target;
// Check if this is the promo code form
if (!form || !form.classList.contains('needs-validation')) {
return;
}
e.preventDefault();
const promoInput = form.querySelector('input[placeholder="{{ __('checkout.enter_promo_code') }}"]');
const submitBtn = form.querySelector('button[type="submit"]');
const originalText = submitBtn.textContent;
if (!promoInput || !promoInput.value.trim()) {
alert('Please enter a promo code');
return;
}
// Show loading state
submitBtn.disabled = true;
submitBtn.textContent = 'Applying...';
// Get CSRF token
const token = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
// Submit voucher code to server
fetch('/apply-voucher', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-TOKEN': token,
'X-Requested-With': 'XMLHttpRequest'
},
body: new URLSearchParams({
'promo_code': promoInput.value.trim()
}).toString()
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Show success message
alert('Voucher applied successfully!');
// Clear input
promoInput.value = '';
// Add visual feedback
promoInput.style.backgroundColor = '#d4edda';
promoInput.style.borderColor = '#28a745';
// Remove visual feedback after 2 seconds
setTimeout(() => {
promoInput.style.backgroundColor = '';
promoInput.style.borderColor = '';
}, 2000);
// Reload page to show updated cart
setTimeout(() => {
window.location.reload();
}, 1000);
} else {
// Show error message
alert(data.message || 'Failed to apply voucher');
}
})
.catch(error => {
console.error('Error applying voucher:', error);
alert('An error occurred while applying the voucher');
})
.finally(() => {
// Restore button state
submitBtn.disabled = false;
submitBtn.textContent = originalText;
});
});
// Handle redeem button click
document.getElementById('redeemVoucherBtn').addEventListener('click', function() {
const voucherEventId = this.getAttribute('data-voucher-event-id');
@ -215,14 +298,7 @@ document.addEventListener('DOMContentLoaded', function () {
// Show success message
alert('Voucher redeemed successfully!');
// Close modal
const modal = bootstrap.Modal.getInstance(document.getElementById('voucherEventModal'));
modal.hide();
// Reload page to update cart
setTimeout(() => {
window.location.reload();
}, 1000);
} else {
// Show error message
alert(data.message || 'Failed to redeem voucher');
@ -259,6 +335,34 @@ document.addEventListener('DOMContentLoaded', function () {
});
});
// Function to apply voucher code when badge is clicked
function applyVoucher(voucherCode) {
// Find the promo code input
const promoInput = document.querySelector('input[placeholder="{{ __('checkout.enter_promo_code') }}"]');
if (promoInput) {
// Set the voucher code
promoInput.value = voucherCode;
// Add visual feedback
promoInput.style.backgroundColor = '#d4edda';
promoInput.style.borderColor = '#28a745';
// Remove visual feedback after 2 seconds
setTimeout(() => {
promoInput.style.backgroundColor = '';
promoInput.style.borderColor = '';
}, 2000);
// Focus on the input
promoInput.focus();
// Optionally submit the form automatically
// Uncomment the next line if you want auto-submission
// promoInput.closest('form').requestSubmit();
}
}
});
</script>

View File

@ -0,0 +1,451 @@
<div class="accordion bg-body-tertiary rounded-5 p-4">
<div class="accordion-item border-0">
<h3 class="accordion-header" id="promoCodeHeading">
<button type="button"
class="accordion-button animate-underline collapsed py-0 ps-sm-2 ps-lg-0 ps-xl-2"
data-bs-toggle="collapse"
data-bs-target="#promoCode"
aria-expanded="false"
aria-controls="promoCode">
<i class="ci-percent fs-xl me-2"></i>
<span class="animate-target me-2">
{{ __('checkout.use_point') }}
</span>
</button>
</h3>
<div class="accordion-collapse collapse"
id="promoCode"
aria-labelledby="promoCodeHeading">
<div class="accordion-body pt-3 pb-2 ps-sm-2 px-lg-0 px-xl-2">
{{-- Your Points --}}
<div class="alert bg-white dark:bg-gray-800 text-white mb-3">
<div class="d-flex align-items-center gap-3">
<div>
<h4 class="mb-0 fw-bold text-dark dark:text-white" id="userPoints">{{ number_format(auth()->user()->customer->point, 0, ",",".") }}</h4>
<small class="text-black dark:text-white">{{ __('checkout.available_points') }}</small>
</div>
</div>
</div>
{{-- Points Form --}}
@if(session('use_point'))
{{-- Points Applied Display --}}
<div class="alert alert-success d-flex align-items-center gap-3">
<div class="flex-grow-1">
<h6 class="mb-1">{{ __('checkout.points_applied') }}</h6>
<p class="mb-0">
<strong>{{ number_format(session('use_point'), 0, ",", ".") }}</strong> {{ __('checkout.points_used') }}
</p>
</div>
<button type="button" class="btn btn-outline-danger btn-sm" id="remove-points-btn">
{{ __('checkout.remove_point') }}
</button>
</div>
@else
{{-- Apply Points Form --}}
<form class="needs-validation d-flex gap-2" novalidate>
<div class="position-relative w-100">
<input type="number"
class="form-control"
id="input-point"
min="1"
max="{{ auth()->user()->customer->point }}"
placeholder="{{ __('checkout.input_point') }}"
required>
</div>
<button type="submit" class="btn btn-dark">
{{ __('checkout.apply_point') }}
</button>
</form>
@endif
</div>
</div>
</div>
</div>
{{-- SINGLE REUSABLE MODAL --}}
<div class="modal fade" id="voucherEventModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content rounded-4">
<div class="modal-header border-0">
<h5 class="modal-title fw-semibold">
Voucher Event
</h5>
<button type="button"
class="btn-close"
data-bs-dismiss="modal">
</button>
</div>
<div class="modal-body pt-0">
<div id="voucherEventContent"></div>
</div>
<div class="modal-footer border-0">
<button type="button"
class="btn btn-secondary"
data-bs-dismiss="modal">
{{ __('checkout.close') }}
</button>
<button type="button"
class="btn btn-primary"
id="redeemVoucherBtn"
data-voucher-event-id="">
{{ __('checkout.redeem') }}
</button>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const modal = document.getElementById('voucherEventModal');
modal.addEventListener('show.bs.modal', function (event) {
const button = event.relatedTarget;
const name = button.getAttribute('data-name');
const description = button.getAttribute('data-description');
const point = button.getAttribute('data-point');
const start = button.getAttribute('data-start');
const end = button.getAttribute('data-end');
const terms = button.getAttribute('data-terms');
let content = '';
if (description) {
content += `<p>${description}</p>`;
}
if (point) {
content += `
<div class="mb-2">
<strong>{{ __('checkout.points_required') }}:</strong>
${point}
</div>
`;
}
if (start && end) {
content += `
<div class="mb-2">
<strong>{{ __('checkout.valid_period') }}:</strong><br>
${start} - ${end}
</div>
`;
}
if (terms) {
content += `
<div class="mt-3 small text-muted">
<strong>{{ __('checkout.terms_and_conditions') }}:</strong>
<p>${terms}</p>
</div>
`;
}
modal.querySelector('.modal-title').textContent = name;
document.getElementById('voucherEventContent').innerHTML = content;
// Set voucher event ID for redeem button
const redeemBtn = document.getElementById('redeemVoucherBtn');
redeemBtn.setAttribute('data-voucher-event-id', button.getAttribute('data-id'));
});
// Handle points form submission
document.addEventListener('submit', function(e) {
const form = e.target;
// Check if this is the points form
if (!form || !form.classList.contains('needs-validation')) {
return;
}
e.preventDefault();
const pointInput = form.querySelector('input[placeholder="{{ __('checkout.input_point') }}"]');
const submitBtn = form.querySelector('button[type="submit"]');
const originalText = submitBtn.textContent;
const availablePoints = parseInt(document.getElementById('userPoints').textContent.replace(/\./g, ''));
if (!pointInput || !pointInput.value.trim()) {
alert('Please enter points to use');
return;
}
const pointsToUse = parseInt(pointInput.value.trim());
if (isNaN(pointsToUse) || pointsToUse <= 0) {
alert('Please enter a valid number of points');
return;
}
if (pointsToUse > availablePoints) {
alert('You cannot use more points than available');
return;
}
// Show loading state
submitBtn.disabled = true;
submitBtn.textContent = 'Applying...';
// Get CSRF token
const token = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
// Submit points to server to set session
fetch('/apply-points', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-TOKEN': token,
'X-Requested-With': 'XMLHttpRequest'
},
body: new URLSearchParams({
'use_point': pointsToUse
}).toString()
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Show success message
alert('Points applied successfully!');
// Clear input
pointInput.value = '';
// Add visual feedback
pointInput.style.backgroundColor = '#d4edda';
pointInput.style.borderColor = '#28a745';
// Remove visual feedback after 2 seconds
setTimeout(() => {
pointInput.style.backgroundColor = '';
pointInput.style.borderColor = '';
}, 2000);
// Reload page to show updated cart
setTimeout(() => {
window.location.reload();
}, 1000);
} else {
// Show error message
alert(data.message || 'Failed to apply points');
}
})
.catch(error => {
console.error('Error applying points:', error);
alert('An error occurred while applying the points');
})
.finally(() => {
// Restore button state
submitBtn.disabled = false;
submitBtn.textContent = originalText;
});
});
// Handle remove points button click
document.getElementById('remove-points-btn')?.addEventListener('click', function() {
const removeBtn = this;
const originalText = removeBtn.textContent;
// Show loading state
removeBtn.disabled = true;
removeBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Removing...';
// Get CSRF token
const token = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
// Remove points via AJAX
fetch('/remove-points', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-TOKEN': token,
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Show success message
alert('Points removed successfully!');
// Clear input field
const pointInput = document.querySelector('input#input-point');
if (pointInput) {
pointInput.value = '';
pointInput.style.backgroundColor = '#f8d7da';
pointInput.style.borderColor = '#dc3545';
// Remove visual feedback after 2 seconds
setTimeout(() => {
pointInput.style.backgroundColor = '';
pointInput.style.borderColor = '';
}, 2000);
}
// Reload page to show updated cart
setTimeout(() => {
window.location.reload();
}, 1000);
} else {
// Show error message
alert(data.message || 'Failed to remove points');
}
})
.catch(error => {
console.error('Error removing points:', error);
alert('An error occurred while removing the points');
})
.finally(() => {
// Restore button state
removeBtn.disabled = false;
removeBtn.textContent = originalText;
});
});
// Handle redeem button click
document.getElementById('redeemVoucherBtn').addEventListener('click', function() {
const voucherEventId = this.getAttribute('data-voucher-event-id');
if (!voucherEventId) {
console.error('No voucher event ID found');
return;
}
// Disable button during processing
this.disabled = true;
this.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Processing...';
// Make AJAX call to redeem voucher
fetch(`/voucher-events/${voucherEventId}/redeem`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
},
body: JSON.stringify({})
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Show success message
alert('Voucher redeemed successfully!');
window.location.reload();
} else {
// Show error message
alert(data.message || 'Failed to redeem voucher');
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred while redeeming the voucher');
})
.finally(() => {
// Re-enable button
this.disabled = false;
this.innerHTML = '{{ __('checkout.redeem') }}';
});
});
// Custom modal handler to prevent backdrop issues
const voucherBadges = document.querySelectorAll('[data-bs-toggle="modal"][data-bs-target="#voucherEventModal"]');
voucherBadges.forEach(function(badge) {
badge.addEventListener('click', function(e) {
e.preventDefault();
// Remove any existing backdrop
const existingBackdrop = document.querySelector('.modal-backdrop');
if (existingBackdrop) {
existingBackdrop.remove();
}
// Show modal without backdrop
const modalInstance = new bootstrap.Modal(modal, {
backdrop: false
});
modalInstance.show();
});
});
// Function to apply voucher code when badge is clicked
function applyVoucher(voucherCode) {
// Find the point input
const pointInput = document.querySelector('input#input-point');
if (pointInput) {
// set point session via AJAX
const token = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
fetch('/apply-points', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-TOKEN': token,
'X-Requested-With': 'XMLHttpRequest'
},
body: new URLSearchParams({
'use_point': voucherCode
}).toString()
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Set the voucher code in input
pointInput.value = voucherCode;
// Add visual feedback
pointInput.style.backgroundColor = '#d4edda';
pointInput.style.borderColor = '#28a745';
// Remove visual feedback after 2 seconds
setTimeout(() => {
pointInput.style.backgroundColor = '';
pointInput.style.borderColor = '';
}, 2000);
// Show success message
alert('Points applied successfully!');
// Reload page to show updated cart
setTimeout(() => {
window.location.reload();
}, 1000);
} else {
// Show error message
alert(data.message || 'Failed to apply points');
}
})
.catch(error => {
console.error('Error applying points:', error);
alert('An error occurred while applying the points');
});
// Focus on the input
pointInput.focus();
}
}
});
</script>

View File

@ -33,8 +33,8 @@
@endif
</div>
<div class="position-relative">
@if ($product->variants->count() > 1)
<div class=" fs-xs text-body-secondary opacity-100">+{{ $product->variants->count() - 1 }} Varian</div>
@if (($product->variants?->count() ?? 0) > 1)
<div class=" fs-xs text-body-secondary opacity-100">+{{ ($product->variants->count() ?? 0) - 1 }} Varian</div>
@endif
{{-- <div class="hover-effect-target fs-xs text-body-secondary opacity-100">+1 color</div> --}}
{{-- <div class="hover-effect-target d-flex gap-2 position-absolute top-0 start-0 opacity-0">

View File

@ -27,13 +27,19 @@
<!-- Search bar visible on screens > 768px wide (md breakpoint) -->
<div class="position-relative w-100 d-none d-md-block me-3 me-xl-4">
<input type="search" class="form-control form-control-lg rounded-pill"
placeholder="Search for products" aria-label="Search">
<button type="button"
<form action="{{ route('product.index') }}" method="GET" id="header-search-form">
<input type="search"
class="form-control form-control-lg rounded-pill"
name="search"
placeholder="Search for products"
aria-label="Search"
value="{{ request('search') }}">
<button type="submit"
class="btn btn-icon btn-ghost fs-lg btn-secondary text-bo border-0 position-absolute top-0 end-0 rounded-circle mt-1 me-1"
aria-label="Search button">
<i class="ci-search"></i>
</button>
</form>
</div>
<!-- Delivery options toggle visible on screens > 1200px wide (xl breakpoint) -->
@ -159,9 +165,15 @@
<div class="collapse d-md-none" id="searchBar">
<div class="container pt-2 pb-3">
<div class="position-relative">
<form action="{{ route('product.index') }}" method="GET" id="mobile-search-form">
<i class="ci-search position-absolute top-50 translate-middle-y d-flex fs-lg ms-3"></i>
<input type="search" class="form-control form-icon-start rounded-pill"
placeholder="Search for products" data-autofocus="collapse">
<input type="search"
class="form-control form-icon-start rounded-pill"
name="search"
placeholder="Search for products"
data-autofocus="collapse"
value="{{ request('search') }}">
</form>
</div>
</div>
</div>
@ -169,3 +181,46 @@
<x-grocery.top-header />
<script>
document.addEventListener('DOMContentLoaded', function() {
// Handle desktop search form submission
const desktopSearchForm = document.getElementById('header-search-form');
if (desktopSearchForm) {
desktopSearchForm.addEventListener('submit', function(e) {
const searchInput = this.querySelector('input[name="search"]');
if (!searchInput.value.trim()) {
e.preventDefault();
searchInput.focus();
return false;
}
});
}
// Handle mobile search form submission
const mobileSearchForm = document.getElementById('mobile-search-form');
if (mobileSearchForm) {
mobileSearchForm.addEventListener('submit', function(e) {
const searchInput = this.querySelector('input[name="search"]');
if (!searchInput.value.trim()) {
e.preventDefault();
searchInput.focus();
return false;
}
});
}
// Add search suggestions functionality (optional enhancement)
const searchInputs = document.querySelectorAll('input[name="search"]');
searchInputs.forEach(input => {
input.addEventListener('input', function(e) {
const searchTerm = e.target.value.trim();
if (searchTerm.length >= 2) {
// You can implement AJAX search suggestions here
// For now, just log the search term
console.log('Searching for:', searchTerm);
}
});
});
});
</script>

View File

@ -133,6 +133,7 @@
showLoginDialog();
} else {
// You could show an error message here
alert(error.message);
}
}
} catch (error) {

View File

@ -51,7 +51,7 @@
{{-- <span class="badge bg-primary rounded-pill ms-auto">1</span> --}}
</a>
<a class="list-group-item list-group-item-action d-flex align-items-center"
href="{{ route('second', ['account', 'wishlist']) }}">
href="{{ route('wishlist.index') }}">
<i class="ci-heart fs-base opacity-75 me-2"></i>
{{ __('account_sidebar.wishlist') }}
</a>

View File

@ -1,9 +1,6 @@
@extends('layouts.landing', ['title' => $product->name ?? 'Detail'])
@section('content')
<x-layout.header-grocery />
<main class="content-wrapper">
@ -73,9 +70,9 @@
</div>
</div>
<button type="button" class="btn btn-lg btn-outline-secondary w-100 collapsed d-md-none"
data-bs-toggle="collapse" data-bs-target="#morePictures"
data-label-collapsed="Show more pictures" data-label-expanded="Show less pictures"
aria-expanded="false" aria-controls="morePictures" aria-label="Show / hide pictures">
data-bs-toggle="collapse" data-bs-target="#morePictures" data-label-collapsed="Show more pictures"
data-label-expanded="Show less pictures" aria-expanded="false" aria-controls="morePictures"
aria-label="Show / hide pictures">
<i class="collapse-toggle-icon ci-chevron-down fs-lg ms-2 me-n2"></i>
</button>
</div>
@ -98,16 +95,34 @@
</a> --}}
<!-- Title -->
<div class="d-flex gap-3">
<div class=" flex-1">
<h1 class="h3">{{ $product->name }}</h1>
</div>
{{-- wishlist/love button --}}
<button type="button"
class="btn btn-outline-danger gap-2"
style="height: 50px;"
id="wishlist-btn"
data-item-id="{{ $product->id }}"
data-is-wishlist="{{ $product->isWishlist() ? 'true' : 'false' }}">
<i class="{{ $product->isWishlist() == true ? 'ci-heart-filled' : 'ci-heart' }}"></i>
</button>
</div>
{{-- add category --}}
<div class="mb-4 gap-2">
<a href="{{ route('product.index',['filter[category_id]' => $product->category->id]) }}"
class="text-decoration-none text-body-emphasis animate-underline text-xs border border-secondary rounded-pill px-2 py-1" style="font-size:10pt;">{{ $product->category->name }}</a>
<a href="{{ route('product.index', ['filter[category_id]' => $product->category->id]) }}"
class="text-decoration-none text-body-emphasis animate-underline text-xs border border-secondary rounded-pill px-2 py-1"
style="font-size:10pt;">{{ $product->category->name }}</a>
<a href="{{ route('product.index', ['filter[brand_id]' => $product->brand->id]) }}"
class="text-decoration-none text-body-emphasis animate-underline text-xs border border-secondary rounded-pill px-2 py-1"
style="font-size:10pt;">{{ $product->brand->name }}</a>
<a href="{{ route('product.index',['filter[brand_id]' => $product->brand->id]) }}"
class="text-decoration-none text-body-emphasis animate-underline text-xs border border-secondary rounded-pill px-2 py-1" style="font-size:10pt;">{{ $product->brand->name }}</a>
</div>
<!-- Description -->
@ -144,8 +159,8 @@
@foreach ($product->variants as $key => $variant)
<input type="radio" class="btn-check" name="colors"
value="{{ $variant->reference->id }}"
id="variant-id-{{ $variant->id }}" {{ $key == 0 ? 'checked' : '' }}>
value="{{ $variant->reference->id }}" id="variant-id-{{ $variant->id }}"
{{ $key == 0 ? 'checked' : '' }}>
<label for="variant-id-{{ $variant->id }}" class="btn btn-image p-0"
data-label="{{ $variant->description }}"
data-price="Rp {{ number_format($product->display_price, 0, ',', '.') }}"
@ -161,11 +176,7 @@
</div>
</div>
{{-- <button type="button" class="btn btn-outline-secondary gap-2">
<i class="ci-heart"></i>
Add to wishlist
</button>
{{--
<br>
<br> --}}
@ -1053,11 +1064,11 @@
document.querySelector('.variantOption').textContent = labelText;
// Update AddToCart component with new variant ID
const addToCartBtn = document.querySelector('[data-item-reference-id]');
const addToCartBtn = document.querySelector('[data-item-id]');
if (addToCartBtn) {
console.log(this.value);
var item_reference_id = this.value;
addToCartBtn.setAttribute('data-item-reference-id', item_reference_id);
var item_id = this.value;
addToCartBtn.setAttribute('data-item-id', item_id);
}
});
});
@ -1067,5 +1078,108 @@
checkedRadio.dispatchEvent(new Event('change'));
}
});
// Wishlist button functionality
document.addEventListener('DOMContentLoaded', function() {
const wishlistBtn = document.getElementById('wishlist-btn');
if (wishlistBtn) {
wishlistBtn.addEventListener('click', function() {
const itemId = this.getAttribute('data-item-id');
const isWishlist = this.getAttribute('data-is-wishlist') === 'true';
const icon = this.querySelector('i');
const text = this.querySelector('span');
// Get CSRF token
const token = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
if (isWishlist) {
// Remove from wishlist
fetch('/account/wishlist/' + itemId, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': token,
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Update button state
this.setAttribute('data-is-wishlist', 'false');
icon.className = 'ci-heart';
text.textContent = 'Add to Wishlist';
// Show success message
const successAlert = document.createElement('div');
successAlert.className = 'alert alert-success alert-dismissible fade show position-fixed';
successAlert.style.css = 'top: 20px; right: 20px; z-index: 9999; max-width: 300px;';
successAlert.innerHTML = `
<button type="button" class="btn-close" data-bs-dismiss="alert">&times;</button>
Item removed from wishlist!
`;
document.body.appendChild(successAlert);
// Auto-hide after 3 seconds
setTimeout(() => {
if (successAlert.parentNode) {
successAlert.parentNode.removeChild(successAlert);
}
}, 3000);
} else {
console.error('Failed to remove from wishlist:', data.message);
}
})
.catch(error => {
console.error('Error removing from wishlist:', error);
});
} else {
// Add to wishlist
fetch('/account/wishlist', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': token,
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({
item_id: itemId
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Update button state
this.setAttribute('data-is-wishlist', 'true');
icon.className = 'ci-heart-filled';
text.textContent = 'In Wishlist';
// Show success message
const successAlert = document.createElement('div');
successAlert.className = 'alert alert-success alert-dismissible fade show position-fixed';
successAlert.style.css = 'top: 20px; right: 20px; z-index: 9999; max-width: 300px;';
successAlert.innerHTML = `
<button type="button" class="btn-close" data-bs-dismiss="alert">&times;</button>
Item added to wishlist!
`;
document.body.appendChild(successAlert);
// Auto-hide after 3 seconds
setTimeout(() => {
if (successAlert.parentNode) {
successAlert.parentNode.removeChild(successAlert);
}
}, 3000);
} else {
console.error('Failed to add to wishlist:', data.message);
}
})
.catch(error => {
console.error('Error adding to wishlist:', error);
});
}
});
}
});
</script>
@endsection

View File

@ -19,6 +19,7 @@ use App\Http\Controllers\TncController;
use App\Http\Controllers\HelpController;
use App\Http\Controllers\VoucherEventController;
use App\Http\Controllers\ContactController;
use App\Http\Controllers\WishController;
use Illuminate\Support\Facades\Route;
@ -123,6 +124,10 @@ Route::middleware(['auth'])->prefix('/checkout')->group(function () {
});
// Apply points route (outside checkout prefix for easier access)
Route::post('/apply-points', [CheckoutController::class, 'applyPoint'])->name('checkout.apply.points');
Route::post('/remove-points', [CheckoutController::class, 'removePoint'])->name('checkout.remove.points');
Route::middleware(['auth'])->prefix('/voucher-events')->group(function () {
Route::post('/{voucherEvent}/redeem', [VoucherEventController::class, 'redeem'])->name('voucher-events.redeem');
});
@ -132,6 +137,15 @@ Route::middleware(['auth'])->prefix('/orders')->group(function () {
Route::get('/', [OrderController::class, 'index'])->name('orders');
});
Route::middleware(['auth'])->prefix('/account/wishlist')->group(function () {
Route::get('/', [WishController::class, 'index'])->name('wishlist.index');
Route::post('/', [WishController::class, 'store'])->name('wishlist.store');
Route::delete('/{id}', [WishController::class, 'destroy'])->name('wishlist.destroy');
});
Route::get('/terms-and-conditions', [TncController::class, 'index'])->name('terms-and-conditions');
Route::get('/help', [HelpController::class, 'index'])->name('help');
Route::get('/contact', [ContactController::class, 'index'])->name('contact');