feat: Dynamic SPA slug, field label storage, and SPA frontpage support (WIP)
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 + '/';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user