Files
Additional/resources/views/checkout.blade.php
T
twotalesanimation b8cc8bd421 feat: Add Google Places autocomplete for address search
- Add address search input field with autocomplete
- Integrate Google Places API to provide address suggestions
- Parse selected address components and auto-populate form fields
- Address fields initially hidden, show when address is selected
- Add clear button to reset address selection
- Support South Africa locations with country restriction
- Add Google Places API key config to services.php
- Hide/show address fields based on selection and validation errors
2026-01-03 00:15:30 +02:00

464 lines
15 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@extends('layouts.app')
@section('title', 'Checkout - Additional Design')
@section('styles')
<style>
h1 {
font-family: 'Abril Fatface', cursive;
font-size: 2.5rem;
color: var(--primary-color);
margin-bottom: 2rem;
}
.checkout-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 3rem;
margin: 2rem 0 3rem 0;
}
.checkout-form,
.order-summary {
border-radius: 20px;
}
.checkout-form h2,
.order-summary h2 {
font-family: 'Abril Fatface', cursive;
font-size: 1.5rem;
color: var(--primary-color);
margin-bottom: 1.5rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
font-weight: 600;
margin-bottom: 0.5rem;
color: var(--primary-color);
}
.form-group input,
.form-group textarea,
.form-group select {
width: 100%;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
font-family: inherit;
}
.form-group textarea {
resize: vertical;
min-height: 100px;
}
.form-group input:focus,
.form-group textarea:focus,
.form-group select:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(45, 80, 71, 0.1);
}
/* Google Places Autocomplete styling */
.pac-container {
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin-top: 4px;
}
.pac-item {
padding: 0.75rem;
cursor: pointer;
}
.pac-item:hover {
background-color: #f0f0f0;
}
.address-search-group {
margin-bottom: 1.5rem;
position: relative;
}
.address-clear-btn {
position: absolute;
right: 12px;
top: 32px;
background: none;
border: none;
color: #999;
cursor: pointer;
font-size: 1.2rem;
padding: 0;
display: none;
}
.address-clear-btn.show {
display: block;
}
.order-summary {
height: fit-content;
}
.order-item {
padding: 1rem 0;
border-bottom: 1px solid #eee;
}
.order-item:last-child {
border-bottom: none;
}
.order-item-name {
font-weight: 600;
margin-bottom: 0.25rem;
}
.order-item-details {
font-size: 0.85rem;
color: #666;
margin-bottom: 0.5rem;
}
.order-item-price {
text-align: right;
font-weight: 600;
color: var(--primary-color);
}
.summary-row {
display: flex;
justify-content: space-between;
padding: 1rem 0;
border-bottom: 1px solid #eee;
}
.summary-row.total {
font-weight: 600;
font-size: 1.2rem;
color: var(--primary-color);
border-top: 2px solid var(--primary-color);
border-bottom: none;
padding-top: 1.5rem;
margin-top: 1rem;
}
.btn-submit {
width: 100%;
margin-top: 2rem;
padding: 1rem;
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 4px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: var(--transition);
}
.btn-submit:hover {
background-color: #1f3633;
}
.alert {
padding: 1rem;
border-radius: 4px;
margin-bottom: 2rem;
}
.alert-error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.info-box {
background: var(--section-light-bg);
padding: 1rem;
border-radius: 4px;
margin-bottom: 1.5rem;
font-size: 0.9rem;
color: #666;
}
.back-link {
display: block;
text-align: center;
margin-top: 1.5rem;
color: var(--primary-color);
text-decoration: none;
}
.back-link:hover {
text-decoration: underline;
}
@media (max-width: 768px) {
.checkout-container {
grid-template-columns: 1fr;
gap: 2rem;
}
h1 {
font-size: 1.8rem;
}
}
</style>
@endsection
@section('content')
<div class="container">
<h1>Checkout</h1>
@if ($errors->any())
<div class="alert alert-error">
<ul style="margin: 0; padding-left: 1.5rem;">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<div class="checkout-container">
<!-- Checkout Form -->
<form action="{{ route('order-process') }}" method="POST" class="checkout-form card">
@csrf
<h2>Delivery Information</h2>
<div class="form-group">
<label for="customer_name">Full Name</label>
<input type="text" id="customer_name" name="customer_name" value="{{ old('customer_name') }}" required>
</div>
<div class="form-group">
<label for="customer_email">Email Address</label>
<input type="email" id="customer_email" name="customer_email" value="{{ old('customer_email') }}" required>
</div>
<div class="form-group">
<label for="customer_phone">Phone Number</label>
<input type="tel" id="customer_phone" name="customer_phone" value="{{ old('customer_phone') }}" required>
</div>
<div class="address-search-group">
<label for="address_search">Delivery Address</label>
<input type="text" id="address_search" placeholder="Start typing your address..." autocomplete="off">
<button type="button" class="address-clear-btn" id="address_clear_btn"></button>
</div>
<div id="address_fields_group" style="display: none;">
<div class="form-group">
<label for="shipping_street_address">Street Address</label>
<input type="text" id="shipping_street_address" name="shipping_street_address" value="{{ old('shipping_street_address') }}" required>
</div>
<div class="form-group">
<label for="shipping_local_area">Suburb/Local Area</label>
<input type="text" id="shipping_local_area" name="shipping_local_area" value="{{ old('shipping_local_area') }}" required>
</div>
<div class="form-group">
<label for="shipping_city">City</label>
<input type="text" id="shipping_city" name="shipping_city" value="{{ old('shipping_city') }}" required>
</div>
<div class="form-group">
<label for="shipping_zone">Province/Zone</label>
<input type="text" id="shipping_zone" name="shipping_zone" value="{{ old('shipping_zone') }}" required>
</div>
<div class="form-group">
<label for="shipping_postcode">Postal Code</label>
<input type="text" id="shipping_postcode" name="shipping_postcode" value="{{ old('shipping_postcode') }}" required>
</div>
<div class="form-group">
<label for="shipping_country">Country</label>
<input type="text" id="shipping_country" name="shipping_country" value="ZA" readonly>
</div>
<div class="form-group">
<label for="shipping_type">Address Type</label>
<select id="shipping_type" name="shipping_type" required>
<option value="residential" {{ old('shipping_type') === 'residential' ? 'selected' : '' }}>Residential</option>
<option value="business" {{ old('shipping_type') === 'business' ? 'selected' : '' }}>Business</option>
</select>
</div>
</div>
<div class="form-group">
<label for="notes">Order Notes (Optional)</label>
<textarea id="notes" name="notes" placeholder="Any special instructions or notes for your order...">{{ old('notes') }}</textarea>
</div>
<div class="info-box">
<strong>Note:</strong> Payment will be processed securely through Yoco. You'll be redirected to complete your card payment after confirming this order.
</div>
<button type="submit" class="btn">Complete Order & Proceed to Payment</button>
</form>
<!-- Order Summary -->
<div class="order-summary card">
<h2>Order Summary</h2>
@foreach($items as $item)
<div class="order-item">
<div class="order-item-name">{{ $item['product']->name }}{{ $item['is_sample'] ? ' - Sample' : '' }}</div>
@php
$stockCost = 0;
$stockUnit = $item['type'] === 'wallpaper' ? '/m' : '/';
if (!$item['is_sample'] && $item['stock']) {
$stockCost = $item['type'] === 'wallpaper' ? $item['stock']->cost_per_meter : $item['stock']->cost_per_m2;
}
@endphp
<div class="order-item-details">
@if($item['is_sample'])
<strong>Sample - Fixed Price:</strong> R{{ number_format(\App\Services\ShippingService::getSampleCost(), 2) }}<br>
@elseif($item['stock'])
<strong>{{ $item['stock']->name }}:</strong> R{{ number_format($stockCost, 2) }}{{ $stockUnit }}<br>
@endif
@if(!$item['is_sample'])
@if($item['type'] === 'wallpaper')
<strong>Length:</strong> {{ $item['length'] }}m
@elseif($item['type'] === 'mural')
<strong>Dimensions:</strong> {{ $item['width'] }}m × {{ $item['height'] }}m ({{ number_format($item['width'] * $item['height'], 2) }}m²)
@endif
@if($item['quantity'] > 1)
<br><strong>Quantity:</strong> {{ $item['quantity'] }}
@endif
@endif
</div>
<div class="order-item-price">R{{ number_format($item['subtotal'], 2) }}</div>
</div>
@endforeach
<div class="summary-row">
<span>Subtotal:</span>
<span>R{{ number_format($total, 2) }}</span>
</div>
<div class="summary-row">
<span>{{ $shippingLabel }}:</span>
<span>R{{ number_format($shippingFee, 2) }}</span>
</div>
<div class="summary-row total">
<span>Total:</span>
<span>R{{ number_format($grandTotal, 2) }}</span>
</div>
<a href="{{ route('cart') }}" class="back-link">← Back to Cart</a>
</div>
</div>
</div>
<script src="https://maps.googleapis.com/maps/api/js?key={{ config('services.google_places.api_key') }}&libraries=places"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const addressSearchInput = document.getElementById('address_search');
const addressFieldsGroup = document.getElementById('address_fields_group');
const clearBtn = document.getElementById('address_clear_btn');
if (!addressSearchInput) return;
// Initialize Google Places Autocomplete
const autocomplete = new google.maps.places.Autocomplete(addressSearchInput, {
componentRestrictions: { country: 'za' },
types: ['geocode']
});
// Prevent form submission on Enter when autocomplete is open
addressSearchInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter' && document.querySelector('.pac-container:not([style*="display: none"])')) {
e.preventDefault();
}
});
// When place is selected
autocomplete.addListener('place_changed', function() {
const place = autocomplete.getPlace();
if (!place.geometry) {
console.error('Place has no geometry');
return;
}
// Parse address components
const addressComponents = {};
place.address_components.forEach(component => {
addressComponents[component.types[0]] = component.long_name;
});
// Populate form fields
document.getElementById('shipping_street_address').value =
(addressComponents['street_number'] ? addressComponents['street_number'] + ' ' : '') +
(addressComponents['route'] || '');
document.getElementById('shipping_local_area').value =
addressComponents['sublocality'] || addressComponents['sublocality_level_1'] || '';
document.getElementById('shipping_city').value =
addressComponents['locality'] || addressComponents['administrative_area_level_2'] || '';
document.getElementById('shipping_zone').value =
addressComponents['administrative_area_level_1'] || '';
document.getElementById('shipping_postcode').value =
addressComponents['postal_code'] || '';
document.getElementById('shipping_country').value =
addressComponents['country'] || 'ZA';
// Show address fields group
addressFieldsGroup.style.display = 'block';
// Show clear button
clearBtn.classList.add('show');
});
// Clear button functionality
clearBtn.addEventListener('click', function(e) {
e.preventDefault();
addressSearchInput.value = '';
addressSearchInput.focus();
clearBtn.classList.remove('show');
addressFieldsGroup.style.display = 'none';
// Clear form fields
document.getElementById('shipping_street_address').value = '';
document.getElementById('shipping_local_area').value = '';
document.getElementById('shipping_city').value = '';
document.getElementById('shipping_zone').value = '';
document.getElementById('shipping_postcode').value = '';
});
// Show fields if form has old values (validation error)
const hasOldValues =
document.getElementById('shipping_street_address').value ||
document.getElementById('shipping_city').value;
if (hasOldValues) {
addressFieldsGroup.style.display = 'block';
}
});
</script>
@endsection