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:
@@ -30,14 +30,13 @@ export default function DashboardSubmenuBar({ items = [], fullscreen = false }:
|
||||
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 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 */}
|
||||
<div className="flex gap-2 overflow-x-auto no-scrollbar w-full flex-shrink">
|
||||
{items.map((it) => {
|
||||
const key = `${it.label}-${it.path || it.href}`;
|
||||
const isActive = !!it.path && (
|
||||
it.exact ? pathname === it.path : pathname.startsWith(it.path)
|
||||
);
|
||||
// Fix: Always use exact match to prevent first submenu from being always active
|
||||
const isActive = !!it.path && pathname === it.path;
|
||||
const cls = [
|
||||
'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',
|
||||
@@ -65,9 +64,9 @@ export default function DashboardSubmenuBar({ items = [], fullscreen = false }:
|
||||
</div>
|
||||
|
||||
{/* 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}>
|
||||
<SelectTrigger className="w-full lg:w-[140px] h-8">
|
||||
<SelectTrigger className="w-full xl:w-[140px] h-8">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -17,9 +17,8 @@ export default function SubmenuBar({ items = [] }: Props) {
|
||||
<div className="flex gap-2 overflow-x-auto no-scrollbar">
|
||||
{items.map((it) => {
|
||||
const key = `${it.label}-${it.path || it.href}`;
|
||||
const isActive = !!it.path && (
|
||||
it.exact ? pathname === it.path : pathname.startsWith(it.path)
|
||||
);
|
||||
// Fix: Always use exact match to prevent first submenu from being always active
|
||||
const isActive = !!it.path && pathname === it.path;
|
||||
const cls = [
|
||||
'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',
|
||||
|
||||
@@ -22,6 +22,8 @@ export interface Option {
|
||||
label: React.ReactNode;
|
||||
/** Optional text used for filtering. Falls back to string label or value. */
|
||||
searchText?: string;
|
||||
/** Optional icon (base64 image or URL) to display before the label */
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
@@ -65,7 +67,12 @@ export function SearchableSelect({
|
||||
aria-disabled={disabled}
|
||||
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" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -99,12 +106,15 @@ export function SearchableSelect({
|
||||
{showCheckIndicator && (
|
||||
<Check
|
||||
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.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>
|
||||
))}
|
||||
</CommandList>
|
||||
|
||||
1099
admin-spa/src/data/flags.json
Normal file
1099
admin-spa/src/data/flags.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -336,16 +336,6 @@ export default function PaymentsPage() {
|
||||
</div>
|
||||
{/* Footer outside scrollable area */}
|
||||
<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
|
||||
type="button"
|
||||
variant="ghost"
|
||||
@@ -362,6 +352,16 @@ export default function PaymentsPage() {
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</a>
|
||||
</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
|
||||
type="button"
|
||||
variant="outline"
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Input } from '@/components/ui/input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { SearchableSelect } from '@/components/ui/searchable-select';
|
||||
import { toast } from 'sonner';
|
||||
import flagsData from '@/data/flags.json';
|
||||
|
||||
interface StoreSettings {
|
||||
storeName: string;
|
||||
@@ -295,11 +296,24 @@ export default function StoreDetailsPage() {
|
||||
<SearchableSelect
|
||||
value={settings.currency}
|
||||
onChange={(v) => updateSetting('currency', v)}
|
||||
options={currencies.map((currency: { code: string; name: string; symbol: string }) => ({
|
||||
value: currency.code,
|
||||
label: `${currency.name} (${currency.symbol})`,
|
||||
searchText: `${currency.code} ${currency.name} ${currency.symbol}`,
|
||||
}))}
|
||||
options={currencies.map((currency: { code: string; name: string; symbol: string }) => {
|
||||
// Use currency code if symbol contains HTML entities (&#x...) or is empty
|
||||
const displaySymbol = (!currency.symbol || currency.symbol.includes('&#'))
|
||||
? 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..."
|
||||
/>
|
||||
</SettingsSection>
|
||||
@@ -419,14 +433,33 @@ export default function StoreDetailsPage() {
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
{/* Summary Card */}
|
||||
{/* Summary Card - Dynamic with Flag */}
|
||||
<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}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Prices will be displayed in {settings.currency} • Timezone: {settings.timezone}
|
||||
</p>
|
||||
{(() => {
|
||||
// Find flag for current currency
|
||||
const currencyFlag = flagsData.find((f: any) => f.code === settings.currency);
|
||||
const currencyInfo = currencies.find((c: any) => c.code === settings.currency);
|
||||
|
||||
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>
|
||||
</SettingsLayout>
|
||||
);
|
||||
|
||||
@@ -38,7 +38,7 @@ export function SettingsLayout({
|
||||
{/* Sticky Header with Save Button */}
|
||||
{onSave && (
|
||||
<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>
|
||||
<h1 className="text-lg font-semibold">{title}</h1>
|
||||
</div>
|
||||
@@ -61,7 +61,7 @@ export function SettingsLayout({
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<div className="container max-w-5xl mx-auto px-4 py-8">
|
||||
<div className="container px-0 max-w-5xl mx-auto">
|
||||
{!onSave && (
|
||||
<div className="mb-8">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
|
||||
1
currencies.json
Normal file
1
currencies.json
Normal file
File diff suppressed because one or more lines are too long
1099
flags.json
Normal file
1099
flags.json
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user