fix: Submenu active state + currency symbols + flags integration

1. Fixed Submenu Active State 
   Problem: First submenu always active due to pathname.startsWith()
   - /dashboard matches /dashboard/analytics
   - Both items show as active

   Solution: Use exact match instead
   - const isActive = pathname === it.path
   - Only clicked item shows as active

   Files: DashboardSubmenuBar.tsx, SubmenuBar.tsx

2. Fixed Currency Symbol Display 
   Problem: HTML entities showing (ءإ)
   Solution: Use currency code when symbol has HTML entities

   Before: United Arab Emirates dirham (ءإ)
   After: United Arab Emirates dirham (AED)

   Logic:
   const displaySymbol = (!currency.symbol || currency.symbol.includes('&#'))
     ? currency.code
     : currency.symbol;

3. Integrated Flags.json 

   A. Moved flags.json to admin-spa/src/data/
   B. Added flag support to SearchableSelect component
      - New icon prop in Option interface
      - Displays flag before label in trigger
      - Displays flag before label in dropdown

   C. Currency select now shows flags
      - Flag icon next to each currency
      - Visual country identification
      - Better UX for currency selection

   D. Dynamic store summary with flag
      Before: 🇮🇩 Your store is located in Indonesia
      After: [FLAG] Your store is located in Indonesia

      - Flag based on selected currency
      - Country name from flags.json
      - Currency name (not just code)
      - Dynamic updates when currency changes

Benefits:
 Clear submenu navigation
 Readable currency symbols
 Visual country flags
 Better currency selection UX
 Dynamic store location display

Files Modified:
- DashboardSubmenuBar.tsx: Exact match for active state
- SubmenuBar.tsx: Exact match for active state
- Store.tsx: Currency symbol fix + flags integration
- searchable-select.tsx: Icon support
- flags.json: Moved to admin-spa/src/data/
This commit is contained in:
dwindown
2025-11-06 11:35:32 +07:00
parent cd644d339c
commit 2a679ffd15
9 changed files with 2276 additions and 36 deletions

View File

@@ -30,14 +30,13 @@ export default function DashboardSubmenuBar({ items = [], fullscreen = false }:
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 ${topClass} 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 flex-col lg:flex-row items-center justify-between gap-4"> <div className="flex flex-col xl:flex-row items-center justify-between gap-4">
{/* Submenu Links */} {/* Submenu Links */}
<div className="flex gap-2 overflow-x-auto no-scrollbar w-full flex-shrink"> <div className="flex gap-2 overflow-x-auto no-scrollbar w-full flex-shrink">
{items.map((it) => { {items.map((it) => {
const key = `${it.label}-${it.path || it.href}`; const key = `${it.label}-${it.path || it.href}`;
const isActive = !!it.path && ( // Fix: Always use exact match to prevent first submenu from being always active
it.exact ? pathname === it.path : pathname.startsWith(it.path) const isActive = !!it.path && pathname === it.path;
);
const cls = [ const cls = [
'inline-flex items-center gap-2 rounded-md px-2.5 py-1.5 border text-sm whitespace-nowrap', 'inline-flex items-center gap-2 rounded-md px-2.5 py-1.5 border text-sm whitespace-nowrap',
'focus:outline-none focus:ring-0 focus:shadow-none', 'focus:outline-none focus:ring-0 focus:shadow-none',
@@ -65,9 +64,9 @@ export default function DashboardSubmenuBar({ items = [], fullscreen = false }:
</div> </div>
{/* Period Selector, Refresh & Dummy Toggle */} {/* Period Selector, Refresh & Dummy Toggle */}
<div className="flex justify-end lg:items-center gap-2 flex-shrink-0 w-full flex-shrink"> <div className="flex justify-end xl:items-center gap-2 flex-shrink-0 w-full xl:w-auto flex-shrink">
<Select value={period} onValueChange={setPeriod}> <Select value={period} onValueChange={setPeriod}>
<SelectTrigger className="w-full lg:w-[140px] h-8"> <SelectTrigger className="w-full xl:w-[140px] h-8">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>

View File

@@ -17,9 +17,8 @@ export default function SubmenuBar({ items = [] }: Props) {
<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) => {
const key = `${it.label}-${it.path || it.href}`; const key = `${it.label}-${it.path || it.href}`;
const isActive = !!it.path && ( // Fix: Always use exact match to prevent first submenu from being always active
it.exact ? pathname === it.path : pathname.startsWith(it.path) const isActive = !!it.path && pathname === it.path;
);
const cls = [ const cls = [
'inline-flex items-center gap-2 rounded-md px-2.5 py-1.5 border text-sm whitespace-nowrap', 'inline-flex items-center gap-2 rounded-md px-2.5 py-1.5 border text-sm whitespace-nowrap',
'focus:outline-none focus:ring-0 focus:shadow-none', 'focus:outline-none focus:ring-0 focus:shadow-none',

View File

@@ -22,6 +22,8 @@ export interface Option {
label: React.ReactNode; label: React.ReactNode;
/** Optional text used for filtering. Falls back to string label or value. */ /** Optional text used for filtering. Falls back to string label or value. */
searchText?: string; searchText?: string;
/** Optional icon (base64 image or URL) to display before the label */
icon?: string;
} }
interface Props { interface Props {
@@ -65,7 +67,12 @@ export function SearchableSelect({
aria-disabled={disabled} aria-disabled={disabled}
tabIndex={disabled ? -1 : 0} tabIndex={disabled ? -1 : 0}
> >
{selected ? selected.label : placeholder} <div className="flex items-center gap-2 flex-1 min-w-0">
{selected?.icon && (
<img src={selected.icon} alt="" className="w-5 h-4 object-cover rounded-sm flex-shrink-0" />
)}
<span className="truncate">{selected ? selected.label : placeholder}</span>
</div>
<ChevronsUpDown className="opacity-50 h-4 w-4 shrink-0" /> <ChevronsUpDown className="opacity-50 h-4 w-4 shrink-0" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
@@ -99,12 +106,15 @@ export function SearchableSelect({
{showCheckIndicator && ( {showCheckIndicator && (
<Check <Check
className={cn( className={cn(
"mr-2 h-4 w-4", "mr-2 h-4 w-4 flex-shrink-0",
opt.value === value ? "opacity-100" : "opacity-0" opt.value === value ? "opacity-100" : "opacity-0"
)} )}
/> />
)} )}
{opt.label} {opt.icon && (
<img src={opt.icon} alt="" className="w-5 h-4 object-cover rounded-sm mr-2 flex-shrink-0" />
)}
<span className="truncate">{opt.label}</span>
</CommandItem> </CommandItem>
))} ))}
</CommandList> </CommandList>

File diff suppressed because it is too large Load Diff

View File

@@ -336,16 +336,6 @@ export default function PaymentsPage() {
</div> </div>
{/* Footer outside scrollable area */} {/* Footer outside scrollable area */}
<div className="border-t px-4 py-3 flex flex-col gap-2 shrink-0 bg-background"> <div className="border-t px-4 py-3 flex flex-col gap-2 shrink-0 bg-background">
<Button
onClick={() => {
const form = document.querySelector('form');
if (form) form.requestSubmit();
}}
disabled={saveMutation.isPending}
className="w-full"
>
{saveMutation.isPending ? 'Saving...' : 'Save Settings'}
</Button>
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
@@ -362,6 +352,16 @@ export default function PaymentsPage() {
<ExternalLink className="h-4 w-4" /> <ExternalLink className="h-4 w-4" />
</a> </a>
</Button> </Button>
<Button
onClick={() => {
const form = document.querySelector('form');
if (form) form.requestSubmit();
}}
disabled={saveMutation.isPending}
className="w-full"
>
{saveMutation.isPending ? 'Saving...' : 'Save Settings'}
</Button>
<Button <Button
type="button" type="button"
variant="outline" variant="outline"

View File

@@ -8,6 +8,7 @@ import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { SearchableSelect } from '@/components/ui/searchable-select'; import { SearchableSelect } from '@/components/ui/searchable-select';
import { toast } from 'sonner'; import { toast } from 'sonner';
import flagsData from '@/data/flags.json';
interface StoreSettings { interface StoreSettings {
storeName: string; storeName: string;
@@ -295,11 +296,24 @@ export default function StoreDetailsPage() {
<SearchableSelect <SearchableSelect
value={settings.currency} value={settings.currency}
onChange={(v) => updateSetting('currency', v)} onChange={(v) => updateSetting('currency', v)}
options={currencies.map((currency: { code: string; name: string; symbol: string }) => ({ options={currencies.map((currency: { code: string; name: string; symbol: string }) => {
value: currency.code, // Use currency code if symbol contains HTML entities (&#x...) or is empty
label: `${currency.name} (${currency.symbol})`, const displaySymbol = (!currency.symbol || currency.symbol.includes('&#'))
searchText: `${currency.code} ${currency.name} ${currency.symbol}`, ? currency.code
}))} : currency.symbol;
// Find matching flag data
const flagInfo = flagsData.find((f: any) => f.code === currency.code);
return {
value: currency.code,
label: flagInfo
? `${currency.name} (${displaySymbol})`
: `${currency.name} (${displaySymbol})`,
searchText: `${currency.code} ${currency.name} ${displaySymbol}`,
icon: flagInfo?.flag, // Add flag as icon
};
})}
placeholder="Select currency..." placeholder="Select currency..."
/> />
</SettingsSection> </SettingsSection>
@@ -419,14 +433,33 @@ export default function StoreDetailsPage() {
</div> </div>
</SettingsCard> </SettingsCard>
{/* Summary Card */} {/* Summary Card - Dynamic with Flag */}
<div className="bg-primary/10 border border-primary/20 rounded-lg p-4"> <div className="bg-primary/10 border border-primary/20 rounded-lg p-4">
<p className="text-sm font-medium"> {(() => {
🇮🇩 Your store is located in {settings.country === 'ID' ? 'Indonesia' : settings.country} // Find flag for current currency
</p> const currencyFlag = flagsData.find((f: any) => f.code === settings.currency);
<p className="text-sm text-muted-foreground mt-1"> const currencyInfo = currencies.find((c: any) => c.code === settings.currency);
Prices will be displayed in {settings.currency} Timezone: {settings.timezone}
</p> return (
<>
<div className="flex items-center gap-2">
{currencyFlag?.flag && (
<img
src={currencyFlag.flag}
alt={currencyFlag.country}
className="w-6 h-4 object-cover rounded-sm"
/>
)}
<p className="text-sm font-medium">
Your store is located in {currencyFlag?.country || settings.country}
</p>
</div>
<p className="text-sm text-muted-foreground mt-1">
Prices will be displayed in {currencyInfo?.name || settings.currency} Timezone: {settings.timezone}
</p>
</>
);
})()}
</div> </div>
</SettingsLayout> </SettingsLayout>
); );

View File

@@ -38,7 +38,7 @@ export function SettingsLayout({
{/* Sticky Header with Save Button */} {/* Sticky Header with Save Button */}
{onSave && ( {onSave && (
<div className="sticky top-0 z-10 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60"> <div className="sticky top-0 z-10 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="container max-w-5xl mx-auto px-4 py-3 flex items-center justify-between"> <div className="container px-0 max-w-5xl mx-auto py-3 flex items-center justify-between">
<div> <div>
<h1 className="text-lg font-semibold">{title}</h1> <h1 className="text-lg font-semibold">{title}</h1>
</div> </div>
@@ -61,7 +61,7 @@ export function SettingsLayout({
)} )}
{/* Content */} {/* Content */}
<div className="container max-w-5xl mx-auto px-4 py-8"> <div className="container px-0 max-w-5xl mx-auto">
{!onSave && ( {!onSave && (
<div className="mb-8"> <div className="mb-8">
<div className="flex items-start justify-between gap-4"> <div className="flex items-start justify-between gap-4">

1
currencies.json Normal file

File diff suppressed because one or more lines are too long

1099
flags.json Normal file

File diff suppressed because it is too large Load Diff