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">
|
||||
|
||||
Reference in New Issue
Block a user