feat: Add flags to Country select + Bank account repeater for BACS
1. Added Emoji Flags to Country/Region Select ✅ Before: Indonesia After: 🇮🇩 Indonesia Implementation: - Uses same countryCodeToEmoji() helper - Flags for all countries in dropdown - Better visual identification 2. Implemented Bank Account Repeater Field ✅ New field type: 'account' - Add/remove multiple bank accounts - Each account has 6 fields: * Account Name (required) * Account Number (required) * Bank Name (required) * Sort Code / Branch Code (optional) * IBAN (optional) * BIC / SWIFT (optional) UI Features: ✅ Compact card layout with muted background ✅ 2-column grid on desktop, 1-column on mobile ✅ Delete button per account (trash icon) ✅ Add button at bottom with plus icon ✅ Account numbering (Account 1, Account 2, etc.) ✅ Smaller inputs (h-9) for compact layout ✅ Clear labels with required indicators Perfect for: - Direct Bank Transfer (BACS) - Manual payment methods - Multiple bank account management 3. Updated GenericGatewayForm ✅ Added support: - New 'account' field type - BankAccount interface - Repeater logic (add/remove/update) - Plus and Trash2 icons from lucide-react Data structure: interface BankAccount { account_name: string; account_number: string; bank_name: string; sort_code?: string; iban?: string; bic?: string; } Benefits: ✅ Country select now has visual flags ✅ Bank accounts are easy to manage ✅ Compact, responsive UI ✅ Clear visual hierarchy ✅ Supports international formats (IBAN, BIC, Sort Code) Files Modified: - Store.tsx: Added flags to country select - GenericGatewayForm.tsx: Bank account repeater - SubmenuBar.tsx: Fullscreen prop (user change)
This commit is contained in:
@@ -2,17 +2,22 @@ import React from 'react';
|
|||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import type { SubItem } from '@/nav/tree';
|
import type { SubItem } from '@/nav/tree';
|
||||||
|
|
||||||
type Props = { items?: SubItem[] };
|
type Props = { items?: SubItem[]; fullscreen?: boolean };
|
||||||
|
|
||||||
export default function SubmenuBar({ items = [] }: Props) {
|
export default function SubmenuBar({ items = [], fullscreen = false }: Props) {
|
||||||
// Always call hooks first
|
// Always call hooks first
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
// Single source of truth: props.items. No fallbacks, no demos, no path-based defaults
|
// Single source of truth: props.items. No fallbacks, no demos, no path-based defaults
|
||||||
if (items.length === 0) return null;
|
if (items.length === 0) return null;
|
||||||
|
|
||||||
|
// Calculate top position based on fullscreen state
|
||||||
|
// Fullscreen: top-16 (below 64px header)
|
||||||
|
// Normal: top-[88px] (below 40px WP admin bar + 48px menu bar)
|
||||||
|
const topClass = fullscreen ? 'top-0' : 'top-[calc(7rem+32px)]';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-submenubar className="border-b border-border bg-background md:bg-background/95 md:backdrop-blur md:supports-[backdrop-filter]:bg-background/60 sticky top-0 z-20">
|
<div data-submenubar className={`border-b border-border bg-background md:bg-background/95 md:backdrop-blur md:supports-[backdrop-filter]:bg-background/60 sticky ${topClass} z-20`}>
|
||||||
<div className="px-4 py-2">
|
<div className="px-4 py-2">
|
||||||
<div className="flex gap-2 overflow-x-auto no-scrollbar">
|
<div className="flex gap-2 overflow-x-auto no-scrollbar">
|
||||||
{items.map((it) => {
|
{items.map((it) => {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
import { ExternalLink, AlertTriangle } from 'lucide-react';
|
import { ExternalLink, AlertTriangle, Plus, Trash2 } from 'lucide-react';
|
||||||
|
|
||||||
interface GatewayField {
|
interface GatewayField {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -51,7 +51,17 @@ interface GenericGatewayFormProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Supported field types (outside component to avoid re-renders)
|
// Supported field types (outside component to avoid re-renders)
|
||||||
const SUPPORTED_FIELD_TYPES = ['text', 'password', 'checkbox', 'select', 'textarea', 'number', 'email', 'url'];
|
const SUPPORTED_FIELD_TYPES = ['text', 'password', 'checkbox', 'select', 'textarea', 'number', 'email', 'url', 'account'];
|
||||||
|
|
||||||
|
// Bank account interface
|
||||||
|
interface BankAccount {
|
||||||
|
account_name: string;
|
||||||
|
account_number: string;
|
||||||
|
bank_name: string;
|
||||||
|
sort_code?: string;
|
||||||
|
iban?: string;
|
||||||
|
bic?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export function GenericGatewayForm({ gateway, onSave, onCancel, hideFooter = false }: GenericGatewayFormProps) {
|
export function GenericGatewayForm({ gateway, onSave, onCancel, hideFooter = false }: GenericGatewayFormProps) {
|
||||||
const [formData, setFormData] = useState<Record<string, unknown>>({});
|
const [formData, setFormData] = useState<Record<string, unknown>>({});
|
||||||
@@ -217,6 +227,160 @@ export function GenericGatewayForm({ gateway, onSave, onCancel, hideFooter = fal
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
case 'account':
|
||||||
|
// Bank account repeater field
|
||||||
|
const accounts = (value as BankAccount[]) || [];
|
||||||
|
|
||||||
|
const addAccount = () => {
|
||||||
|
const newAccounts = [...accounts, {
|
||||||
|
account_name: '',
|
||||||
|
account_number: '',
|
||||||
|
bank_name: '',
|
||||||
|
sort_code: '',
|
||||||
|
iban: '',
|
||||||
|
bic: ''
|
||||||
|
}];
|
||||||
|
handleFieldChange(field.id, newAccounts);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeAccount = (index: number) => {
|
||||||
|
const newAccounts = accounts.filter((_, i) => i !== index);
|
||||||
|
handleFieldChange(field.id, newAccounts);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateAccount = (index: number, key: keyof BankAccount, val: string) => {
|
||||||
|
const newAccounts = [...accounts];
|
||||||
|
newAccounts[index] = { ...newAccounts[index], [key]: val };
|
||||||
|
handleFieldChange(field.id, newAccounts);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={field.id} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label>
|
||||||
|
{field.title}
|
||||||
|
{field.required && <span className="text-destructive ml-1">*</span>}
|
||||||
|
</Label>
|
||||||
|
{field.description && (
|
||||||
|
<p
|
||||||
|
className="text-sm text-muted-foreground mt-1"
|
||||||
|
dangerouslySetInnerHTML={{ __html: field.description }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{accounts.map((account, index) => (
|
||||||
|
<div key={index} className="border rounded-lg p-4 space-y-3 bg-muted/30">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<h4 className="text-sm font-medium">Account {index + 1}</h4>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => removeAccount(index)}
|
||||||
|
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label htmlFor={`account_name_${index}`} className="text-xs">
|
||||||
|
Account Name <span className="text-destructive">*</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id={`account_name_${index}`}
|
||||||
|
value={account.account_name}
|
||||||
|
onChange={(e) => updateAccount(index, 'account_name', e.target.value)}
|
||||||
|
placeholder="e.g., Business Account"
|
||||||
|
className="h-9"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label htmlFor={`account_number_${index}`} className="text-xs">
|
||||||
|
Account Number <span className="text-destructive">*</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id={`account_number_${index}`}
|
||||||
|
value={account.account_number}
|
||||||
|
onChange={(e) => updateAccount(index, 'account_number', e.target.value)}
|
||||||
|
placeholder="e.g., 12345678"
|
||||||
|
className="h-9"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label htmlFor={`bank_name_${index}`} className="text-xs">
|
||||||
|
Bank Name <span className="text-destructive">*</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id={`bank_name_${index}`}
|
||||||
|
value={account.bank_name}
|
||||||
|
onChange={(e) => updateAccount(index, 'bank_name', e.target.value)}
|
||||||
|
placeholder="e.g., Bank Central Asia"
|
||||||
|
className="h-9"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label htmlFor={`sort_code_${index}`} className="text-xs">
|
||||||
|
Sort Code / Branch Code
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id={`sort_code_${index}`}
|
||||||
|
value={account.sort_code || ''}
|
||||||
|
onChange={(e) => updateAccount(index, 'sort_code', e.target.value)}
|
||||||
|
placeholder="e.g., 12-34-56"
|
||||||
|
className="h-9"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label htmlFor={`iban_${index}`} className="text-xs">
|
||||||
|
IBAN
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id={`iban_${index}`}
|
||||||
|
value={account.iban || ''}
|
||||||
|
onChange={(e) => updateAccount(index, 'iban', e.target.value)}
|
||||||
|
placeholder="e.g., GB29 NWBK 6016 1331 9268 19"
|
||||||
|
className="h-9"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label htmlFor={`bic_${index}`} className="text-xs">
|
||||||
|
BIC / SWIFT
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id={`bic_${index}`}
|
||||||
|
value={account.bic || ''}
|
||||||
|
onChange={(e) => updateAccount(index, 'bic', e.target.value)}
|
||||||
|
placeholder="e.g., NWBKGB2L"
|
||||||
|
className="h-9"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={addAccount}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
Add Bank Account
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// text, password, number, email, url
|
// text, password, number, email, url
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -249,11 +249,14 @@ export default function StoreDetailsPage() {
|
|||||||
<SearchableSelect
|
<SearchableSelect
|
||||||
value={settings.country}
|
value={settings.country}
|
||||||
onChange={(v) => updateSetting('country', v)}
|
onChange={(v) => updateSetting('country', v)}
|
||||||
options={countries.map((country: { code: string; name: string }) => ({
|
options={countries.map((country: { code: string; name: string }) => {
|
||||||
|
const flagEmoji = countryCodeToEmoji(country.code);
|
||||||
|
return {
|
||||||
value: country.code,
|
value: country.code,
|
||||||
label: country.name,
|
label: `${flagEmoji} ${country.name}`.trim(),
|
||||||
searchText: country.name,
|
searchText: `${country.code} ${country.name}`,
|
||||||
}))}
|
};
|
||||||
|
})}
|
||||||
placeholder="Select country..."
|
placeholder="Select country..."
|
||||||
/>
|
/>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|||||||
Reference in New Issue
Block a user