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 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
|
||||
const { pathname } = useLocation();
|
||||
|
||||
// Single source of truth: props.items. No fallbacks, no demos, no path-based defaults
|
||||
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 (
|
||||
<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="flex gap-2 overflow-x-auto no-scrollbar">
|
||||
{items.map((it) => {
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from '@/components/ui/select';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { ExternalLink, AlertTriangle } from 'lucide-react';
|
||||
import { ExternalLink, AlertTriangle, Plus, Trash2 } from 'lucide-react';
|
||||
|
||||
interface GatewayField {
|
||||
id: string;
|
||||
@@ -51,7 +51,17 @@ interface GenericGatewayFormProps {
|
||||
}
|
||||
|
||||
// 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) {
|
||||
const [formData, setFormData] = useState<Record<string, unknown>>({});
|
||||
@@ -217,6 +227,160 @@ export function GenericGatewayForm({ gateway, onSave, onCancel, hideFooter = fal
|
||||
</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:
|
||||
// text, password, number, email, url
|
||||
return (
|
||||
|
||||
@@ -249,11 +249,14 @@ export default function StoreDetailsPage() {
|
||||
<SearchableSelect
|
||||
value={settings.country}
|
||||
onChange={(v) => updateSetting('country', v)}
|
||||
options={countries.map((country: { code: string; name: string }) => ({
|
||||
value: country.code,
|
||||
label: country.name,
|
||||
searchText: country.name,
|
||||
}))}
|
||||
options={countries.map((country: { code: string; name: string }) => {
|
||||
const flagEmoji = countryCodeToEmoji(country.code);
|
||||
return {
|
||||
value: country.code,
|
||||
label: `${flagEmoji} ${country.name}`.trim(),
|
||||
searchText: `${country.code} ${country.name}`,
|
||||
};
|
||||
})}
|
||||
placeholder="Select country..."
|
||||
/>
|
||||
</SettingsSection>
|
||||
|
||||
Reference in New Issue
Block a user