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:
dwindown
2025-11-06 12:23:38 +07:00
parent 39a215c188
commit 2008f2f141
3 changed files with 182 additions and 10 deletions

View File

@@ -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) => {

View File

@@ -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 (

View File

@@ -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 }) => {
value: country.code, const flagEmoji = countryCodeToEmoji(country.code);
label: country.name, return {
searchText: country.name, value: country.code,
}))} label: `${flagEmoji} ${country.name}`.trim(),
searchText: `${country.code} ${country.name}`,
};
})}
placeholder="Select country..." placeholder="Select country..."
/> />
</SettingsSection> </SettingsSection>