diff --git a/RAJAONGKIR_INTEGRATION.md b/RAJAONGKIR_INTEGRATION.md
index f24a012..2e34328 100644
--- a/RAJAONGKIR_INTEGRATION.md
+++ b/RAJAONGKIR_INTEGRATION.md
@@ -37,133 +37,433 @@ do_action( 'woonoow/shipping/before_calculate', $shipping_data, $items );
- `city` - City name
- `postcode` - Postal code
- `address_1` - Street address
- - `address_2` - Additional address (optional)
+ - `destination_id` - Custom field for Rajaongkir (added via addon)
- + Any custom fields added by addons
- `$items` (array) - Cart items being shipped
---
-## Code Snippet: Rajaongkir Bridge
+## Complete Integration: Searchable Destination Selector
-Add this code to your theme's `functions.php` or via a code snippets plugin:
+This is a complete code solution that adds a searchable Rajaongkir destination selector to WooNooW checkout.
+
+### Plugin File: `woonoow-rajaongkir-bridge.php`
+
+Create this as a new plugin or add to your theme's functions.php:
```php
session->__unset( 'selected_destination_id' );
WC()->session->__unset( 'selected_destination_label' );
return;
}
- // Check if destination_id is provided by frontend
+ // Set destination from frontend
if ( ! empty( $shipping['destination_id'] ) ) {
- WC()->session->set( 'selected_destination_id', $shipping['destination_id'] );
- WC()->session->set( 'selected_destination_label', $shipping['destination_label'] ?? $shipping['city'] );
- return;
- }
-
- // Fallback: Try to lookup destination from city name
- // This requires the Rajaongkir database lookup
- $destination = rajaongkir_lookup_destination( $shipping['city'], $shipping['state'] );
- if ( $destination ) {
- WC()->session->set( 'selected_destination_id', $destination['id'] );
- WC()->session->set( 'selected_destination_label', $destination['label'] );
+ WC()->session->set( 'selected_destination_id', intval( $shipping['destination_id'] ) );
+ WC()->session->set( 'selected_destination_label', sanitize_text_field( $shipping['destination_label'] ?? '' ) );
}
}, 10, 2 );
/**
- * Helper: Lookup Rajaongkir destination by city/state name.
- *
- * This is a simplified example. Implement based on your Rajaongkir data structure.
+ * 2. REST API: Search Rajaongkir destinations
*/
-function rajaongkir_lookup_destination( $city, $state ) {
- // Query the Rajaongkir location database
- // This depends on how your Rajaongkir plugin stores location data
+add_action( 'rest_api_init', function() {
+ register_rest_route( 'woonoow/v1', '/rajaongkir/destinations', [
+ 'methods' => 'GET',
+ 'callback' => 'woonoow_search_rajaongkir_destinations',
+ 'permission_callback' => '__return_true',
+ 'args' => [
+ 'search' => [
+ 'required' => true,
+ 'type' => 'string',
+ 'sanitize_callback' => 'sanitize_text_field',
+ ],
+ ],
+ ]);
+});
+
+function woonoow_search_rajaongkir_destinations( $request ) {
+ $search = $request->get_param( 'search' );
- // Example using transient/option storage:
- $locations = get_option( 'cekongkir_destinations', [] );
+ if ( strlen( $search ) < 2 ) {
+ return new WP_REST_Response( [], 200 );
+ }
+
+ // Get Rajaongkir destinations from transient/cache
+ $destinations = get_transient( 'cekongkir_all_destinations' );
- foreach ( $locations as $location ) {
- if (
- stripos( $location['city'], $city ) !== false ||
- stripos( $location['district'], $city ) !== false
- ) {
- return [
- 'id' => $location['id'],
- 'label' => $location['label'],
+ if ( ! $destinations ) {
+ // Fetch from Rajaongkir API or database
+ $destinations = woonoow_fetch_rajaongkir_destinations();
+ set_transient( 'cekongkir_all_destinations', $destinations, DAY_IN_SECONDS );
+ }
+
+ // Filter by search term
+ $results = array_filter( $destinations, function( $dest ) use ( $search ) {
+ return stripos( $dest['label'], $search ) !== false;
+ });
+
+ // Limit to 50 results
+ $results = array_slice( array_values( $results ), 0, 50 );
+
+ return new WP_REST_Response( $results, 200 );
+}
+
+/**
+ * 3. Fetch destinations from Rajaongkir database
+ */
+function woonoow_fetch_rajaongkir_destinations() {
+ global $wpdb;
+
+ $destinations = [];
+
+ // Try to get from Rajaongkir plugin's stored data
+ $table = $wpdb->prefix . 'cekongkir_destinations';
+
+ if ( $wpdb->get_var( "SHOW TABLES LIKE '$table'" ) === $table ) {
+ $rows = $wpdb->get_results( "SELECT * FROM $table", ARRAY_A );
+
+ foreach ( $rows as $row ) {
+ $destinations[] = [
+ 'value' => $row['id'],
+ 'label' => $row['province'] . ', ' . $row['city'] . ', ' . $row['subdistrict'],
+ 'province' => $row['province'],
+ 'city' => $row['city'],
+ 'subdistrict' => $row['subdistrict'],
];
}
}
- return null;
+ // Fallback: Use options storage
+ if ( empty( $destinations ) ) {
+ $stored = get_option( 'cekongkir_destinations', [] );
+ foreach ( $stored as $id => $label ) {
+ $destinations[] = [
+ 'value' => $id,
+ 'label' => $label,
+ ];
+ }
+ }
+
+ return $destinations;
}
+
+/**
+ * 4. Enqueue scripts for checkout
+ */
+add_action( 'wp_enqueue_scripts', function() {
+ if ( ! is_checkout() && ! is_wc_endpoint_url() ) {
+ return;
+ }
+
+ wp_enqueue_script(
+ 'woonoow-rajaongkir-bridge',
+ plugin_dir_url( __FILE__ ) . 'rajaongkir-bridge.js',
+ [],
+ '1.0.0',
+ true
+ );
+
+ wp_localize_script( 'woonoow-rajaongkir-bridge', 'WNW_RAJAONGKIR', [
+ 'apiUrl' => rest_url( 'woonoow/v1/rajaongkir/destinations' ),
+ 'nonce' => wp_create_nonce( 'wp_rest' ),
+ ]);
+});
+```
+
+### JavaScript: `rajaongkir-bridge.js`
+
+This script adds a searchable destination selector to the checkout:
+
+```javascript
+/**
+ * Rajaongkir Destination Selector for WooNooW
+ *
+ * Adds a searchable dropdown to select Indonesian subdistrict
+ */
+(function() {
+ 'use strict';
+
+ // Only run on checkout
+ if (!document.querySelector('.woonoow-checkout, .woocommerce-checkout')) {
+ return;
+ }
+
+ // Wait for DOM
+ document.addEventListener('DOMContentLoaded', function() {
+ initRajaongkirSelector();
+ });
+
+ function initRajaongkirSelector() {
+ // Find the city/state fields in shipping form
+ const countryField = document.querySelector('[name="shipping_country"], [name="billing_country"]');
+
+ if (!countryField) return;
+
+ // Check if Indonesia is selected
+ const checkCountry = () => {
+ const country = countryField.value;
+
+ if (country === 'ID') {
+ showDestinationSelector();
+ } else {
+ hideDestinationSelector();
+ }
+ };
+
+ countryField.addEventListener('change', checkCountry);
+ checkCountry();
+ }
+
+ function showDestinationSelector() {
+ // Create searchable destination field if not exists
+ if (document.querySelector('#rajaongkir-destination-wrapper')) {
+ document.querySelector('#rajaongkir-destination-wrapper').style.display = 'block';
+ return;
+ }
+
+ const wrapper = document.createElement('div');
+ wrapper.id = 'rajaongkir-destination-wrapper';
+ wrapper.className = 'form-group md:col-span-2';
+ wrapper.innerHTML = `
+
+
+ `;
+
+ // Insert after postcode field
+ const postcodeField = document.querySelector('[name="shipping_postcode"], [name="billing_postcode"]');
+ if (postcodeField) {
+ postcodeField.closest('.form-group, div')?.after(wrapper);
+ }
+
+ setupSearchHandler();
+ }
+
+ function hideDestinationSelector() {
+ const wrapper = document.querySelector('#rajaongkir-destination-wrapper');
+ if (wrapper) {
+ wrapper.style.display = 'none';
+ }
+ }
+
+ let searchTimeout;
+ function setupSearchHandler() {
+ const searchInput = document.querySelector('#rajaongkir-search');
+ const resultsDiv = document.querySelector('#rajaongkir-results');
+ const destinationIdInput = document.querySelector('#rajaongkir-destination-id');
+ const destinationLabelInput = document.querySelector('#rajaongkir-destination-label');
+
+ searchInput.addEventListener('input', function(e) {
+ const query = e.target.value;
+
+ clearTimeout(searchTimeout);
+
+ if (query.length < 2) {
+ resultsDiv.classList.add('hidden');
+ return;
+ }
+
+ searchTimeout = setTimeout(() => {
+ searchDestinations(query);
+ }, 300);
+ });
+
+ async function searchDestinations(query) {
+ try {
+ const response = await fetch(
+ `${WNW_RAJAONGKIR.apiUrl}?search=${encodeURIComponent(query)}`,
+ {
+ headers: {
+ 'X-WP-Nonce': WNW_RAJAONGKIR.nonce,
+ },
+ }
+ );
+
+ const destinations = await response.json();
+
+ if (destinations.length === 0) {
+ resultsDiv.innerHTML = 'No destinations found
';
+ } else {
+ resultsDiv.innerHTML = destinations.map(dest => `
+
+ ${dest.label}
+
+ `).join('');
+ }
+
+ resultsDiv.classList.remove('hidden');
+
+ // Setup click handlers
+ resultsDiv.querySelectorAll('[data-value]').forEach(item => {
+ item.addEventListener('click', function() {
+ const value = this.dataset.value;
+ const label = this.dataset.label;
+
+ searchInput.value = label;
+ destinationIdInput.value = value;
+ destinationLabelInput.value = label;
+ resultsDiv.classList.add('hidden');
+
+ // Trigger shipping calculation
+ document.body.dispatchEvent(new Event('woonoow:address_changed'));
+ });
+ });
+ } catch (error) {
+ console.error('Rajaongkir search error:', error);
+ }
+ }
+
+ // Hide on click outside
+ document.addEventListener('click', function(e) {
+ if (!e.target.closest('#rajaongkir-destination-wrapper')) {
+ resultsDiv.classList.add('hidden');
+ }
+ });
+ }
+})();
```
---
-## Frontend Integration (Optional)
+## React Component Version (for customer-spa)
-To add a proper Rajaongkir destination selector to WooNooW's admin order form, you need to:
+If you're extending the customer-spa directly, here's a React component:
-### 1. Register Custom Address Fields
+```tsx
+// components/RajaongkirDestinationSelector.tsx
+import React, { useState, useEffect } from 'react';
+import { api } from '@/lib/api/client';
+import { SearchableSelect } from '@/components/ui/searchable-select';
-```php
-add_filter( 'woonoow/checkout/address_fields', function( $fields ) {
- // Only for Indonesia
- if ( WC()->customer && WC()->customer->get_shipping_country() === 'ID' ) {
- $fields['destination_id'] = [
- 'type' => 'hidden',
- 'required' => true,
- ];
- $fields['destination_selector'] = [
- 'type' => 'custom',
- 'component' => 'RajaongkirDestinationSelector',
- 'label' => __( 'Destination', 'woonoow-rajaongkir' ),
- 'required' => true,
- ];
+interface DestinationOption {
+ value: string;
+ label: string;
+}
+
+interface Props {
+ value?: string;
+ label?: string;
+ onChange: (id: string, label: string) => void;
+}
+
+export function RajaongkirDestinationSelector({ value, label, onChange }: Props) {
+ const [search, setSearch] = useState('');
+ const [options, setOptions] = useState([]);
+ const [loading, setLoading] = useState(false);
+
+ useEffect(() => {
+ if (search.length < 2) {
+ setOptions([]);
+ return;
}
- return $fields;
-} );
+
+ const timer = setTimeout(async () => {
+ setLoading(true);
+ try {
+ const data = await api.get('/rajaongkir/destinations', { search });
+ setOptions(data);
+ } catch (error) {
+ console.error('Failed to search destinations:', error);
+ } finally {
+ setLoading(false);
+ }
+ }, 300);
+
+ return () => clearTimeout(timer);
+ }, [search]);
+
+ const handleSelect = (selectedValue: string) => {
+ const selected = options.find(o => o.value === selectedValue);
+ if (selected) {
+ onChange(selected.value, selected.label);
+ }
+ };
+
+ return (
+
+
+
+ {label && (
+
Selected: {label}
+ )}
+
+ );
+}
```
-### 2. Enqueue JavaScript for Destination Search
+Usage in Checkout:
-```php
-add_action( 'woonoow/admin/enqueue_scripts', function() {
- wp_enqueue_script(
- 'woonoow-rajaongkir',
- plugins_url( 'assets/js/woonoow-integration.js', __FILE__ ),
- [ 'woonoow-admin' ],
- '1.0.0',
- true
- );
-
- wp_localize_script( 'woonoow-rajaongkir', 'WNW_RAJAONGKIR', [
- 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
- 'nonce' => wp_create_nonce( 'rajaongkir_search' ),
- ] );
-} );
+```tsx
+// In Checkout/index.tsx
+import { RajaongkirDestinationSelector } from '@/components/RajaongkirDestinationSelector';
+
+// In the shipping form, add:
+{shippingData.country === 'ID' && (
+
+ setShippingData({
+ ...shippingData,
+ destination_id: id,
+ destination_label: label,
+ })}
+ />
+
+)}
```
---
## Testing
-1. Install and configure Rajaongkir plugin
-2. Add the code snippet above
-3. Go to WooNooW → Orders → Create New Order
-4. Add a product
-5. Set country to Indonesia
-6. Fill in city/state (or use destination selector if implemented)
+1. Install Rajaongkir plugin and configure API key
+2. Add the bridge code above
+3. Go to WooNooW Checkout
+4. Set country to Indonesia
+5. Type in the destination search field
+6. Select a destination from dropdown
7. Click "Calculate Shipping"
8. Verify Rajaongkir rates appear
@@ -171,29 +471,21 @@ add_action( 'woonoow/admin/enqueue_scripts', function() {
## Troubleshooting
-### Rates not appearing?
+### Destinations not loading?
-1. Check browser DevTools Network tab for `/shipping/calculate` response
-2. Look for `debug` object in response:
- ```json
- {
- "methods": [...],
- "debug": {
- "packages_count": 1,
- "cart_items_count": 1,
- "address": { ... }
- }
- }
- ```
-
-3. Verify Rajaongkir session is set:
- ```php
- error_log( 'destination_id: ' . WC()->session->get( 'selected_destination_id' ) );
- ```
+Check the REST API endpoint:
+```
+GET /wp-json/woonoow/v1/rajaongkir/destinations?search=jakarta
+```
### Session not persisting?
-Make sure WooCommerce session is initialized before the hook runs. The hook fires during REST API calls where session may not be initialized by default.
+Verify the hook is firing:
+```php
+add_action( 'woonoow/shipping/before_calculate', function( $shipping, $items ) {
+ error_log( 'Shipping data: ' . print_r( $shipping, true ) );
+}, 5, 2 );
+```
---
diff --git a/customer-spa/package-lock.json b/customer-spa/package-lock.json
index e5d97da..16f6c79 100644
--- a/customer-spa/package-lock.json
+++ b/customer-spa/package-lock.json
@@ -25,6 +25,7 @@
"@tanstack/react-query": "^5.90.5",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "cmdk": "^1.1.1",
"lucide-react": "^0.547.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -3597,6 +3598,22 @@
"node": ">=6"
}
},
+ "node_modules/cmdk": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz",
+ "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "^1.1.1",
+ "@radix-ui/react-dialog": "^1.1.6",
+ "@radix-ui/react-id": "^1.1.0",
+ "@radix-ui/react-primitive": "^2.0.2"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19 || ^19.0.0-rc",
+ "react-dom": "^18 || ^19 || ^19.0.0-rc"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
diff --git a/customer-spa/package.json b/customer-spa/package.json
index 2ab06b5..802c812 100644
--- a/customer-spa/package.json
+++ b/customer-spa/package.json
@@ -27,6 +27,7 @@
"@tanstack/react-query": "^5.90.5",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "cmdk": "^1.1.1",
"lucide-react": "^0.547.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
diff --git a/customer-spa/src/components/ui/command.tsx b/customer-spa/src/components/ui/command.tsx
new file mode 100644
index 0000000..c647892
--- /dev/null
+++ b/customer-spa/src/components/ui/command.tsx
@@ -0,0 +1,107 @@
+"use client"
+
+import * as React from "react"
+import { Command as CommandPrimitive } from "cmdk"
+import { Search } from "lucide-react"
+import { cn } from "@/lib/utils"
+
+const Command = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+Command.displayName = CommandPrimitive.displayName
+
+const CommandInput = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+))
+
+CommandInput.displayName = CommandPrimitive.Input.displayName
+
+const CommandList = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+
+CommandList.displayName = CommandPrimitive.List.displayName
+
+const CommandEmpty = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>((props, ref) => (
+
+))
+
+CommandEmpty.displayName = CommandPrimitive.Empty.displayName
+
+const CommandGroup = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+
+CommandGroup.displayName = CommandPrimitive.Group.displayName
+
+const CommandItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+
+CommandItem.displayName = CommandPrimitive.Item.displayName
+
+export {
+ Command,
+ CommandInput,
+ CommandList,
+ CommandEmpty,
+ CommandGroup,
+ CommandItem,
+}
diff --git a/customer-spa/src/components/ui/popover.tsx b/customer-spa/src/components/ui/popover.tsx
new file mode 100644
index 0000000..0eda425
--- /dev/null
+++ b/customer-spa/src/components/ui/popover.tsx
@@ -0,0 +1,30 @@
+import * as React from "react"
+import * as PopoverPrimitive from "@radix-ui/react-popover"
+import { cn } from "@/lib/utils"
+
+const Popover = PopoverPrimitive.Root
+
+const PopoverTrigger = PopoverPrimitive.Trigger
+
+const PopoverAnchor = PopoverPrimitive.Anchor
+
+const PopoverContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
+
+
+
+))
+PopoverContent.displayName = PopoverPrimitive.Content.displayName
+
+export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
diff --git a/customer-spa/src/components/ui/searchable-select.tsx b/customer-spa/src/components/ui/searchable-select.tsx
new file mode 100644
index 0000000..a184609
--- /dev/null
+++ b/customer-spa/src/components/ui/searchable-select.tsx
@@ -0,0 +1,99 @@
+import * as React from "react";
+import {
+ Popover,
+ PopoverTrigger,
+ PopoverContent,
+} from "@/components/ui/popover";
+import {
+ Command,
+ CommandInput,
+ CommandList,
+ CommandItem,
+ CommandEmpty,
+} from "@/components/ui/command";
+import { Check, ChevronDown } from "lucide-react";
+import { cn } from "@/lib/utils";
+
+export interface Option {
+ value: string;
+ label: string;
+ searchText?: string;
+}
+
+interface Props {
+ value?: string;
+ onChange?: (v: string) => void;
+ options: Option[];
+ placeholder?: string;
+ emptyLabel?: string;
+ className?: string;
+ disabled?: boolean;
+}
+
+export function SearchableSelect({
+ value,
+ onChange,
+ options,
+ placeholder = "Select...",
+ emptyLabel = "No results found.",
+ className,
+ disabled = false,
+}: Props) {
+ const [open, setOpen] = React.useState(false);
+ const selected = options.find((o) => o.value === value);
+
+ React.useEffect(() => { if (disabled && open) setOpen(false); }, [disabled, open]);
+
+ return (
+ !disabled && setOpen(o)}>
+
+
+
+
+
+
+
+ {emptyLabel}
+ {options.map((opt) => (
+ {
+ onChange?.(opt.value);
+ setOpen(false);
+ }}
+ >
+
+ {opt.label}
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/customer-spa/src/pages/Checkout/index.tsx b/customer-spa/src/pages/Checkout/index.tsx
index b278042..1ee069a 100644
--- a/customer-spa/src/pages/Checkout/index.tsx
+++ b/customer-spa/src/pages/Checkout/index.tsx
@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom';
import { useCartStore } from '@/lib/cart/store';
import { useCheckoutSettings } from '@/hooks/useAppearanceSettings';
import { Button } from '@/components/ui/button';
+import { SearchableSelect } from '@/components/ui/searchable-select';
import Container from '@/components/Layout/Container';
import SEOHead from '@/components/SEOHead';
import { formatPrice } from '@/lib/currency';
@@ -97,6 +98,62 @@ export default function Checkout() {
const [showBillingForm, setShowBillingForm] = useState(true);
const [showShippingForm, setShowShippingForm] = useState(true);
+ // Countries and states data
+ const [countries, setCountries] = useState<{ code: string; name: string }[]>([]);
+ const [states, setStates] = useState>>({});
+ const [defaultCountry, setDefaultCountry] = useState('');
+
+ // Load countries and states
+ useEffect(() => {
+ const loadCountries = async () => {
+ try {
+ const data = await api.get<{
+ countries: { code: string; name: string }[];
+ states: Record>;
+ default_country: string;
+ }>('/countries');
+ setCountries(data.countries || []);
+ setStates(data.states || {});
+ setDefaultCountry(data.default_country || '');
+
+ // Set default country if not already set
+ if (!billingData.country && data.default_country) {
+ setBillingData(prev => ({ ...prev, country: data.default_country }));
+ }
+ if (!shippingData.country && data.default_country) {
+ setShippingData(prev => ({ ...prev, country: data.default_country }));
+ }
+ } catch (error) {
+ console.error('Failed to load countries:', error);
+ }
+ };
+ loadCountries();
+ }, []);
+
+ // Country/state options for SearchableSelect
+ const countryOptions = countries.map(c => ({ value: c.code, label: `${c.name} (${c.code})` }));
+ const billingStateOptions = Object.entries(states[billingData.country] || {}).map(([code, name]) => ({ value: code, label: name }));
+ const shippingStateOptions = Object.entries(states[shippingData.country] || {}).map(([code, name]) => ({ value: code, label: name }));
+
+ // Clear state when country changes
+ useEffect(() => {
+ if (billingData.country && billingData.state) {
+ const countryStates = states[billingData.country] || {};
+ if (!countryStates[billingData.state]) {
+ setBillingData(prev => ({ ...prev, state: '' }));
+ }
+ }
+ }, [billingData.country, states]);
+
+ useEffect(() => {
+ if (shippingData.country && shippingData.state) {
+ const countryStates = states[shippingData.country] || {};
+ if (!countryStates[shippingData.state]) {
+ setShippingData(prev => ({ ...prev, state: '' }));
+ }
+ }
+ }, [shippingData.country, states]);
+
// Load saved addresses
useEffect(() => {
const loadAddresses = async () => {
@@ -491,14 +548,24 @@ export default function Checkout() {
className="w-full border rounded-lg px-4 py-2"
/>
+
+
+ setBillingData({ ...billingData, country: v })}
+ placeholder="Select country"
+ disabled={countries.length <= 1}
+ />
+
- setBillingData({ ...billingData, state: e.target.value })}
- className="w-full border rounded-lg px-4 py-2"
+ onChange={(v) => setBillingData({ ...billingData, state: v })}
+ placeholder={billingStateOptions.length ? "Select state" : "N/A"}
+ disabled={!billingStateOptions.length}
/>
@@ -511,16 +578,6 @@ export default function Checkout() {
className="w-full border rounded-lg px-4 py-2"
/>
-
-
- setBillingData({ ...billingData, country: e.target.value })}
- className="w-full border rounded-lg px-4 py-2"
- />
-
>
)}
@@ -652,14 +709,24 @@ export default function Checkout() {
className="w-full border rounded-lg px-4 py-2"
/>
+
+
+ setShippingData({ ...shippingData, country: v })}
+ placeholder="Select country"
+ disabled={countries.length <= 1}
+ />
+
- setShippingData({ ...shippingData, state: e.target.value })}
- className="w-full border rounded-lg px-4 py-2"
+ onChange={(v) => setShippingData({ ...shippingData, state: v })}
+ placeholder={shippingStateOptions.length ? "Select state" : "N/A"}
+ disabled={!shippingStateOptions.length}
/>
@@ -672,16 +739,6 @@ export default function Checkout() {
className="w-full border rounded-lg px-4 py-2"
/>
-
-
- setShippingData({ ...shippingData, country: e.target.value })}
- className="w-full border rounded-lg px-4 py-2"
- />
-
)}
>