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 (
|
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>
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
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>
|
</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"
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
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