feat: Dynamic SPA slug, field label storage, and SPA frontpage support (WIP)

This commit is contained in:
Dwindi Ramadhana
2026-01-10 00:50:32 +07:00
parent d3ec580ec8
commit 3357fbfcf1
20 changed files with 1317 additions and 465 deletions

View File

@@ -59,16 +59,12 @@ const getAppearanceSettings = () => {
const getInitialRoute = () => {
const appEl = document.getElementById('woonoow-customer-app');
const initialRoute = appEl?.getAttribute('data-initial-route');
console.log('[WooNooW Customer] Initial route from data attribute:', initialRoute);
console.log('[WooNooW Customer] App element:', appEl);
console.log('[WooNooW Customer] All data attributes:', appEl?.dataset);
return initialRoute || '/shop'; // Default to shop if not specified
};
// Router wrapper component that uses hooks requiring Router context
function AppRoutes() {
const initialRoute = getInitialRoute();
console.log('[WooNooW Customer] Using initial route:', initialRoute);
return (
<BaseLayout>

View File

@@ -124,7 +124,7 @@ export function DynamicCheckoutField({
placeholder={field.placeholder}
required={field.required}
autoComplete={field.autocomplete}
className="w-full border rounded-lg px-4 py-2"
className="w-full border !rounded-lg px-4 py-2"
/>
);
@@ -180,7 +180,7 @@ export function DynamicCheckoutField({
onChange={(e) => onChange(e.target.value)}
placeholder={field.placeholder}
required={field.required}
className="w-full border rounded-lg px-4 py-2 min-h-[100px]"
className="w-full border !rounded-lg px-4 py-2 min-h-[100px]"
/>
);
@@ -228,7 +228,7 @@ export function DynamicCheckoutField({
placeholder={field.placeholder}
required={field.required}
autoComplete={field.autocomplete || 'email'}
className="w-full border rounded-lg px-4 py-2"
className="w-full border !rounded-lg px-4 py-2"
/>
);
@@ -241,7 +241,7 @@ export function DynamicCheckoutField({
placeholder={field.placeholder}
required={field.required}
autoComplete={field.autocomplete || 'tel'}
className="w-full border rounded-lg px-4 py-2"
className="w-full border !rounded-lg px-4 py-2"
/>
);
@@ -253,7 +253,7 @@ export function DynamicCheckoutField({
onChange={(e) => onChange(e.target.value)}
placeholder={field.placeholder}
required={field.required}
className="w-full border rounded-lg px-4 py-2"
className="w-full border !rounded-lg px-4 py-2"
/>
);
@@ -267,7 +267,7 @@ export function DynamicCheckoutField({
placeholder={field.placeholder}
required={field.required}
autoComplete={field.autocomplete}
className="w-full border rounded-lg px-4 py-2"
className="w-full border !rounded-lg px-4 py-2"
/>
);
}

View File

@@ -68,7 +68,7 @@ export function SearchableSelect({
type="button"
role="combobox"
className={cn(
"w-full flex items-center justify-between border rounded-lg px-4 py-2 text-left bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-primary/20",
"w-full flex items-center justify-between border !rounded-lg px-4 py-2 text-left bg-white hover:bg-gray-50 focus:outline-none focus:border-gray-400",
disabled && "opacity-50 cursor-not-allowed",
className
)}

View File

@@ -25,7 +25,7 @@ export function useAddToCartFromUrl() {
const hash = window.location.hash;
const hashParams = new URLSearchParams(hash.split('?')[1] || '');
const productId = hashParams.get('add-to-cart');
if (!productId) return;
const variationId = hashParams.get('variation_id');
@@ -34,41 +34,29 @@ export function useAddToCartFromUrl() {
// Create unique key for this add-to-cart request
const requestKey = `${productId}-${variationId || 'none'}-${quantity}`;
// Skip if already processed
if (processedRef.current.has(requestKey)) {
console.log('[WooNooW] Skipping duplicate add-to-cart:', requestKey);
return;
}
console.log('[WooNooW] Add to cart from URL:', {
productId,
variationId,
quantity,
redirect,
fullUrl: window.location.href,
requestKey,
});
// Mark as processed
processedRef.current.add(requestKey);
addToCart(productId, variationId, quantity)
.then((cartData) => {
// Update cart store with fresh data from API
if (cartData) {
setCart(cartData);
console.log('[WooNooW] Cart updated with fresh data:', cartData);
}
// Remove URL parameters after adding to cart
const currentPath = window.location.hash.split('?')[0];
window.location.hash = currentPath;
// Navigate based on redirect parameter
const targetPage = redirect === 'checkout' ? '/checkout' : '/cart';
if (!location.pathname.includes(targetPage)) {
console.log(`[WooNooW] Navigating to ${targetPage}`);
navigate(targetPage);
}
})
@@ -98,8 +86,6 @@ async function addToCart(
body.variation_id = parseInt(variationId, 10);
}
console.log('[WooNooW] Adding to cart:', body);
const response = await fetch(`${apiRoot}/cart/add`, {
method: 'POST',
headers: {
@@ -116,8 +102,7 @@ async function addToCart(
}
const data = await response.json();
console.log('[WooNooW] Product added to cart:', data);
// API returns {message, cart_item_key, cart} on success
if (data.cart_item_key && data.cart) {
toast.success(data.message || 'Product added to cart');

View File

@@ -1,8 +1,9 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useMemo } from 'react';
import { MapPin, Plus, Edit, Trash2, Star } from 'lucide-react';
import { api } from '@/lib/api/client';
import { toast } from 'sonner';
import SEOHead from '@/components/SEOHead';
import { DynamicCheckoutField } from '@/components/DynamicCheckoutField';
interface Address {
id: number;
@@ -20,6 +21,35 @@ interface Address {
email?: string;
phone?: string;
is_default: boolean;
// Custom fields
[key: string]: any;
}
interface CheckoutField {
key: string;
fieldset: 'billing' | 'shipping' | 'account' | 'order';
type: string;
label: string;
placeholder?: string;
required: boolean;
hidden: boolean;
class?: string[];
priority: number;
options?: Record<string, string> | null;
custom: boolean;
autocomplete?: string;
validate?: string[];
input_class?: string[];
custom_attributes?: Record<string, string>;
default?: string;
search_endpoint?: string | null;
search_param?: string;
min_chars?: number;
}
interface CountryOption {
value: string;
label: string;
}
export default function Addresses() {
@@ -27,23 +57,94 @@ export default function Addresses() {
const [loading, setLoading] = useState(true);
const [showModal, setShowModal] = useState(false);
const [editingAddress, setEditingAddress] = useState<Address | null>(null);
const [formData, setFormData] = useState<Partial<Address>>({
const [formData, setFormData] = useState<Record<string, any>>({
label: '',
type: 'both',
first_name: '',
last_name: '',
company: '',
address_1: '',
address_2: '',
city: '',
state: '',
postcode: '',
country: 'ID',
email: '',
phone: '',
is_default: false,
});
// Checkout fields from API
const [checkoutFields, setCheckoutFields] = useState<CheckoutField[]>([]);
const [countryOptions, setCountryOptions] = useState<CountryOption[]>([]);
const [stateOptions, setStateOptions] = useState<CountryOption[]>([]);
const [loadingFields, setLoadingFields] = useState(true);
// Fetch checkout fields and countries
useEffect(() => {
const loadFieldsAndCountries = async () => {
try {
// Fetch checkout fields (POST method required by API)
const fieldsResponse = await api.post<{ fields: CheckoutField[] }>('/checkout/fields', {});
setCheckoutFields(fieldsResponse.fields || []);
// Fetch countries
const countriesResponse = await api.get<{ countries: Record<string, string> }>('/countries');
if (countriesResponse.countries) {
const options = Object.entries(countriesResponse.countries).map(([code, name]) => ({
value: code,
label: String(name),
}));
setCountryOptions(options);
}
} catch (error) {
console.error('Failed to load checkout fields:', error);
} finally {
setLoadingFields(false);
}
};
loadFieldsAndCountries();
}, []);
// Listen for field label events from DynamicCheckoutField (searchable_select)
// This captures the human-readable label alongside the ID value
useEffect(() => {
const handleFieldLabel = (event: CustomEvent<{ key: string; value: string }>) => {
const { key, value } = event.detail;
setFormData(prev => ({
...prev,
[key]: value,
}));
};
document.addEventListener('woonoow:field_label', handleFieldLabel as EventListener);
return () => {
document.removeEventListener('woonoow:field_label', handleFieldLabel as EventListener);
};
}, []);
// Fetch states when country changes
useEffect(() => {
const country = formData.country || formData.billing_country || '';
if (!country) {
setStateOptions([]);
return;
}
const loadStates = async () => {
try {
const response = await api.get<{ states: Record<string, string> }>(`/countries/${country}/states`);
if (response.states) {
const options = Object.entries(response.states).map(([code, name]) => ({
value: code,
label: String(name),
}));
setStateOptions(options);
} else {
setStateOptions([]);
}
} catch {
setStateOptions([]);
}
};
loadStates();
}, [formData.country, formData.billing_country]);
// Filter billing fields - API already returns them sorted by priority
const billingFields = useMemo(() => {
return checkoutFields
.filter(f => f.fieldset === 'billing' && !f.hidden && f.type !== 'hidden');
}, [checkoutFields]);
useEffect(() => {
loadAddresses();
}, []);
@@ -76,40 +177,86 @@ export default function Addresses() {
}
};
// Helper to get field value - handles both prefixed and non-prefixed keys
const getFieldValue = (key: string): string => {
// Try exact key first
if (formData[key] !== undefined) return String(formData[key] || '');
// Try without prefix
const unprefixed = key.replace(/^billing_/, '');
if (formData[unprefixed] !== undefined) return String(formData[unprefixed] || '');
return '';
};
// Helper to set field value - stores both prefixed and unprefixed for compatibility
const setFieldValue = (key: string, value: string) => {
const unprefixed = key.replace(/^billing_/, '');
setFormData(prev => ({
...prev,
[key]: value,
[unprefixed]: value,
}));
};
const handleAdd = () => {
setEditingAddress(null);
setFormData({
// Initialize with defaults from API fields
const defaults: Record<string, any> = {
label: '',
type: 'both',
first_name: '',
last_name: '',
company: '',
address_1: '',
address_2: '',
city: '',
state: '',
postcode: '',
country: 'ID',
email: '',
phone: '',
is_default: false,
};
billingFields.forEach(field => {
if (field.default) {
const unprefixed = field.key.replace(/^billing_/, '');
defaults[field.key] = field.default;
defaults[unprefixed] = field.default;
}
});
setFormData(defaults);
setShowModal(true);
};
const handleEdit = (address: Address) => {
setEditingAddress(address);
setFormData(address);
// Map address fields to formData with both prefixed and unprefixed keys
const data: Record<string, any> = { ...address };
// Add billing_ prefixed versions
Object.entries(address).forEach(([key, value]) => {
data[`billing_${key}`] = value;
});
setFormData(data);
setShowModal(true);
};
const handleSave = async () => {
try {
// Prepare payload with unprefixed keys
const payload: Record<string, any> = {
label: formData.label,
type: formData.type,
is_default: formData.is_default,
};
// Add all fields (unprefixed)
billingFields.forEach(field => {
const unprefixed = field.key.replace(/^billing_/, '');
payload[unprefixed] = getFieldValue(field.key);
// Also include _label fields if they exist (for searchable_select fields)
const labelKey = field.key + '_label';
if (formData[labelKey]) {
const unprefixedLabel = unprefixed + '_label';
payload[unprefixedLabel] = formData[labelKey];
}
});
if (editingAddress) {
await api.put(`/account/addresses/${editingAddress.id}`, formData);
await api.put(`/account/addresses/${editingAddress.id}`, payload);
toast.success('Address updated successfully');
} else {
await api.post('/account/addresses', formData);
await api.post('/account/addresses', payload);
toast.success('Address added successfully');
}
setShowModal(false);
@@ -144,6 +291,13 @@ export default function Addresses() {
}
};
// Check if a field should be wide (full width)
const isFieldWide = (field: CheckoutField): boolean => {
const fieldName = field.key.replace(/^billing_/, '');
return ['address_1', 'address_2', 'email'].includes(fieldName) ||
field.class?.includes('form-row-wide') || false;
};
if (loading) {
return (
<div className="flex items-center justify-center py-12">
@@ -245,161 +399,69 @@ export default function Addresses() {
{editingAddress ? 'Edit Address' : 'Add New Address'}
</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1">Label *</label>
<input
type="text"
value={formData.label}
onChange={(e) => setFormData({ ...formData, label: e.target.value })}
placeholder="e.g., Home, Office, Parents"
className="w-full px-3 py-2 border rounded-lg"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Address Type *</label>
<select
value={formData.type}
onChange={(e) => setFormData({ ...formData, type: e.target.value as Address['type'] })}
className="w-full px-3 py-2 border rounded-lg"
>
<option value="both">Billing & Shipping</option>
<option value="billing">Billing Only</option>
<option value="shipping">Shipping Only</option>
</select>
</div>
<div className="grid grid-cols-2 gap-4">
{loadingFields ? (
<div className="py-8 text-center text-gray-500">Loading form fields...</div>
) : (
<div className="space-y-4">
{/* Label field - always shown */}
<div>
<label className="block text-sm font-medium mb-1">First Name *</label>
<label className="block text-sm font-medium mb-1">Label *</label>
<input
type="text"
value={formData.first_name}
onChange={(e) => setFormData({ ...formData, first_name: e.target.value })}
className="w-full px-3 py-2 border rounded-lg"
value={formData.label || ''}
onChange={(e) => setFormData({ ...formData, label: e.target.value })}
placeholder="e.g., Home, Office, Parents"
className="w-full px-3 py-2 border !rounded-lg"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Last Name *</label>
<input
type="text"
value={formData.last_name}
onChange={(e) => setFormData({ ...formData, last_name: e.target.value })}
className="w-full px-3 py-2 border rounded-lg"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium mb-1">Company</label>
<input
type="text"
value={formData.company}
onChange={(e) => setFormData({ ...formData, company: e.target.value })}
className="w-full px-3 py-2 border rounded-lg"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Address Line 1 *</label>
<input
type="text"
value={formData.address_1}
onChange={(e) => setFormData({ ...formData, address_1: e.target.value })}
className="w-full px-3 py-2 border rounded-lg"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Address Line 2</label>
<input
type="text"
value={formData.address_2}
onChange={(e) => setFormData({ ...formData, address_2: e.target.value })}
className="w-full px-3 py-2 border rounded-lg"
/>
</div>
<div className="grid grid-cols-2 gap-4">
{/* Address Type - always shown */}
<div>
<label className="block text-sm font-medium mb-1">City *</label>
<input
type="text"
value={formData.city}
onChange={(e) => setFormData({ ...formData, city: e.target.value })}
<label className="block text-sm font-medium mb-1">Address Type *</label>
<select
value={formData.type || 'both'}
onChange={(e) => setFormData({ ...formData, type: e.target.value as Address['type'] })}
className="w-full px-3 py-2 border rounded-lg"
/>
>
<option value="both">Billing & Shipping</option>
<option value="billing">Billing Only</option>
<option value="shipping">Shipping Only</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1">State/Province *</label>
{/* Dynamic fields from checkout API - DynamicCheckoutField renders its own labels */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{billingFields.map((field) => (
<DynamicCheckoutField
key={field.key}
field={field}
value={getFieldValue(field.key)}
onChange={(v) => setFieldValue(field.key, v)}
countryOptions={countryOptions}
stateOptions={stateOptions}
/>
))}
</div>
{/* Set as default checkbox */}
<div className="flex items-center gap-2 pt-2">
<input
type="text"
value={formData.state}
onChange={(e) => setFormData({ ...formData, state: e.target.value })}
className="w-full px-3 py-2 border rounded-lg"
type="checkbox"
id="is_default"
checked={formData.is_default || false}
onChange={(e) => setFormData({ ...formData, is_default: e.target.checked })}
className="w-4 h-4"
/>
<label htmlFor="is_default" className="text-sm">Set as default address</label>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1">Postcode *</label>
<input
type="text"
value={formData.postcode}
onChange={(e) => setFormData({ ...formData, postcode: e.target.value })}
className="w-full px-3 py-2 border rounded-lg"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Country *</label>
<input
type="text"
value={formData.country}
onChange={(e) => setFormData({ ...formData, country: e.target.value })}
className="w-full px-3 py-2 border rounded-lg"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1">Email</label>
<input
type="email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
className="w-full px-3 py-2 border rounded-lg"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Phone</label>
<input
type="tel"
value={formData.phone}
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
className="w-full px-3 py-2 border rounded-lg"
/>
</div>
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="is_default"
checked={formData.is_default}
onChange={(e) => setFormData({ ...formData, is_default: e.target.checked })}
className="w-4 h-4"
/>
<label htmlFor="is_default" className="text-sm">Set as default address</label>
</div>
</div>
)}
<div className="flex gap-3 mt-6">
<button
onClick={handleSave}
className="font-[inherit] flex-1 px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors"
disabled={loadingFields}
className="font-[inherit] flex-1 px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors disabled:opacity-50"
>
Save Address
</button>

View File

@@ -80,10 +80,12 @@ export function AccountLayout({ children }: AccountLayoutProps) {
});
// Full page reload to clear cookies and refresh state
window.location.href = window.location.origin + '/store/';
const basePath = (window as any).woonoowCustomer?.basePath || '/store';
window.location.href = window.location.origin + basePath + '/';
} catch (error) {
// Even on error, try to redirect and let server handle session
window.location.href = window.location.origin + '/store/';
const basePath = (window as any).woonoowCustomer?.basePath || '/store';
window.location.href = window.location.origin + basePath + '/';
}
};

View File

@@ -712,7 +712,7 @@ export default function Checkout() {
required={getBillingField('billing_first_name')?.required}
value={billingData.firstName}
onChange={(e) => setBillingData({ ...billingData, firstName: e.target.value })}
className="w-full border rounded-lg px-4 py-2"
className="w-full border !rounded-lg px-4 py-2"
/>
</div>
)}
@@ -724,7 +724,7 @@ export default function Checkout() {
required={getBillingField('billing_last_name')?.required}
value={billingData.lastName}
onChange={(e) => setBillingData({ ...billingData, lastName: e.target.value })}
className="w-full border rounded-lg px-4 py-2"
className="w-full border !rounded-lg px-4 py-2"
/>
</div>
)}
@@ -736,7 +736,7 @@ export default function Checkout() {
required={getBillingField('billing_email')?.required}
value={billingData.email}
onChange={(e) => setBillingData({ ...billingData, email: e.target.value })}
className="w-full border rounded-lg px-4 py-2"
className="w-full border !rounded-lg px-4 py-2"
/>
</div>
)}
@@ -748,7 +748,7 @@ export default function Checkout() {
required={getBillingField('billing_phone')?.required}
value={billingData.phone}
onChange={(e) => setBillingData({ ...billingData, phone: e.target.value })}
className="w-full border rounded-lg px-4 py-2"
className="w-full border !rounded-lg px-4 py-2"
/>
</div>
)}
@@ -764,7 +764,7 @@ export default function Checkout() {
required={getBillingField('billing_address_1')?.required}
value={billingData.address}
onChange={(e) => setBillingData({ ...billingData, address: e.target.value })}
className="w-full border rounded-lg px-4 py-2"
className="w-full border !rounded-lg px-4 py-2"
/>
</div>
)}
@@ -777,7 +777,7 @@ export default function Checkout() {
required={getBillingField('billing_city')?.required}
value={billingData.city}
onChange={(e) => setBillingData({ ...billingData, city: e.target.value })}
className="w-full border rounded-lg px-4 py-2"
className="w-full border !rounded-lg px-4 py-2"
/>
</div>
)}
@@ -811,7 +811,7 @@ export default function Checkout() {
value={billingData.state}
onChange={(e) => setBillingData({ ...billingData, state: e.target.value })}
placeholder="Enter state/province"
className="w-full border rounded-lg px-4 py-2"
className="w-full border !rounded-lg px-4 py-2"
/>
)}
</div>
@@ -825,7 +825,7 @@ export default function Checkout() {
required={getBillingField('billing_postcode')?.required}
value={billingData.postcode}
onChange={(e) => setBillingData({ ...billingData, postcode: e.target.value })}
className="w-full border rounded-lg px-4 py-2"
className="w-full border !rounded-lg px-4 py-2"
/>
</div>
)}
@@ -932,36 +932,43 @@ export default function Checkout() {
{/* Shipping Form - Only show if no saved address selected or user wants to enter manually */}
{(!selectedShippingAddressId || showShippingForm) && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-2">First Name *</label>
<input
type="text"
required
value={shippingData.firstName}
onChange={(e) => setShippingData({ ...shippingData, firstName: e.target.value })}
className="w-full border rounded-lg px-4 py-2"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">Last Name *</label>
<input
type="text"
required
value={shippingData.lastName}
onChange={(e) => setShippingData({ ...shippingData, lastName: e.target.value })}
className="w-full border rounded-lg px-4 py-2"
/>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium mb-2">Street Address *</label>
<input
type="text"
required
value={shippingData.address}
onChange={(e) => setShippingData({ ...shippingData, address: e.target.value })}
className="w-full border rounded-lg px-4 py-2"
/>
</div>
{/* Dynamic shipping fields using getShippingField like billing */}
{getShippingField('shipping_first_name') && (
<div className={getFieldWrapperClass('shipping_first_name', 'shipping')}>
<label className="block text-sm font-medium mb-2">{getShippingField('shipping_first_name')?.label || 'First Name'} {getShippingField('shipping_first_name')?.required && '*'}</label>
<input
type="text"
required={getShippingField('shipping_first_name')?.required}
value={shippingData.firstName}
onChange={(e) => setShippingData({ ...shippingData, firstName: e.target.value })}
className="w-full border !rounded-lg px-4 py-2"
/>
</div>
)}
{getShippingField('shipping_last_name') && (
<div className={getFieldWrapperClass('shipping_last_name', 'shipping')}>
<label className="block text-sm font-medium mb-2">{getShippingField('shipping_last_name')?.label || 'Last Name'} {getShippingField('shipping_last_name')?.required && '*'}</label>
<input
type="text"
required={getShippingField('shipping_last_name')?.required}
value={shippingData.lastName}
onChange={(e) => setShippingData({ ...shippingData, lastName: e.target.value })}
className="w-full border !rounded-lg px-4 py-2"
/>
</div>
)}
{getShippingField('shipping_address_1') && (
<div className={getFieldWrapperClass('shipping_address_1', 'shipping') || 'md:col-span-2'}>
<label className="block text-sm font-medium mb-2">{getShippingField('shipping_address_1')?.label || 'Street Address'} {getShippingField('shipping_address_1')?.required && '*'}</label>
<input
type="text"
required={getShippingField('shipping_address_1')?.required}
value={shippingData.address}
onChange={(e) => setShippingData({ ...shippingData, address: e.target.value })}
className="w-full border !rounded-lg px-4 py-2"
/>
</div>
)}
{/* City field - conditionally rendered based on API */}
{getShippingField('shipping_city') && (
<div>
@@ -971,7 +978,7 @@ export default function Checkout() {
required={getShippingField('shipping_city')?.required}
value={shippingData.city}
onChange={(e) => setShippingData({ ...shippingData, city: e.target.value })}
className="w-full border rounded-lg px-4 py-2"
className="w-full border !rounded-lg px-4 py-2"
/>
</div>
)}
@@ -1005,7 +1012,7 @@ export default function Checkout() {
value={shippingData.state}
onChange={(e) => setShippingData({ ...shippingData, state: e.target.value })}
placeholder="Enter state/province"
className="w-full border rounded-lg px-4 py-2"
className="w-full border !rounded-lg px-4 py-2"
/>
)}
</div>
@@ -1019,7 +1026,7 @@ export default function Checkout() {
required={getShippingField('shipping_postcode')?.required}
value={shippingData.postcode}
onChange={(e) => setShippingData({ ...shippingData, postcode: e.target.value })}
className="w-full border rounded-lg px-4 py-2"
className="w-full border !rounded-lg px-4 py-2"
/>
</div>
)}
@@ -1050,7 +1057,7 @@ export default function Checkout() {
value={orderNotes}
onChange={(e) => setOrderNotes(e.target.value)}
placeholder="Notes about your order, e.g. special notes for delivery."
className="w-full border rounded-lg px-4 py-2 h-32"
className="w-full border !rounded-lg px-4 py-2 h-32"
/>
</div>
)}

View File

@@ -100,7 +100,8 @@ export default function Login() {
// Set the target URL with hash route, then force reload
// The hash change alone doesn't reload the page, so cookies won't be refreshed
const targetUrl = window.location.origin + '/store/#' + redirectTo;
const basePath = (window as any).woonoowCustomer?.basePath || '/store';
const targetUrl = window.location.origin + basePath + '/#' + redirectTo;
window.location.href = targetUrl;
// Force page reload to refresh cookies and server-side state
window.location.reload();