feat: implement onboarding wizard and fix help page navigation

Core Features:
- Add Quick Setup Wizard for new users with multi-step flow
- Implement distraction-free onboarding layout (no sidebar/header)
- Create OnboardingController API endpoint for saving settings
- Redirect new users to /setup automatically on first admin access

Onboarding Components:
- StepMode: Select between full/minimal store modes
- StepHomepage: Choose or auto-create homepage
- StepAppearance: Configure container width and primary color
- StepProgress: Visual progress indicator

Navigation & Routing:
- Fix Help page links to use react-router navigation (prevent full reload)
- Update onboarding completion redirect to /appearance/pages
- Add manual onboarding access via Settings > Store Details

UI/UX Improvements:
- Enable dark mode support for Page Editor
- Fix page title rendering in onboarding dropdown
- Improve title fallback logic (title.rendered, title, post_title)

Type Safety:
- Unify PageItem interface across all components
- Add 'default' to containerWidth type definition
- Add missing properties (permalink_base, has_template, icon)

Files Modified:
- includes/Api/OnboardingController.php
- includes/Api/Routes.php
- includes/Admin/Assets.php
- admin-spa/src/App.tsx
- admin-spa/src/routes/Onboarding/*
- admin-spa/src/routes/Help/DocContent.tsx
- admin-spa/src/routes/Settings/Store.tsx
- admin-spa/src/routes/Appearance/Pages/*
This commit is contained in:
Dwindi Ramadhana
2026-02-06 00:30:38 +07:00
parent 7da4f0a167
commit 687a2318b0
15 changed files with 755 additions and 124 deletions

View File

@@ -296,6 +296,7 @@ import NewsletterCampaignsList from '@/routes/Marketing/Campaigns';
import CampaignEdit from '@/routes/Marketing/Campaigns/Edit';
import MorePage from '@/routes/More';
import Help from '@/routes/Help';
import Onboarding from '@/routes/Onboarding';
// Addon Route Component - Dynamically loads addon components
function AddonRoute({ config }: { config: any }) {
@@ -569,7 +570,8 @@ function AppRoutes() {
return (
<Routes>
{/* Dashboard */}
<Route path="/" element={<Navigate to="/dashboard" replace />} />
<Route path="/" element={<Navigate to={(window as any).WNW_CONFIG?.onboardingCompleted ? "/dashboard" : "/setup"} replace />} />
<Route path="/setup" element={<Onboarding />} />
<Route path="/reset-password" element={<ResetPassword />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/dashboard/revenue" element={<DashboardRevenue />} />
@@ -744,6 +746,19 @@ function Shell() {
const submenuTopClass = fullscreen ? 'top-0' : 'top-[calc(7rem+32px)]';
const submenuZIndex = fullscreen ? 'z-50' : 'z-40';
// Check if current route is setup/onboarding
const isSetup = location.pathname === '/setup';
if (isSetup) {
return (
<AppProvider isStandalone={isStandalone} exitFullscreen={exitFullscreen}>
<div className="min-h-screen bg-background text-foreground flex flex-col">
<AppRoutes />
</div>
</AppProvider>
);
}
return (
<AppProvider isStandalone={isStandalone} exitFullscreen={exitFullscreen}>
{!isStandalone && <ShortcutsBinder onToggle={toggle} />}

View File

@@ -32,7 +32,7 @@ import {
import { InspectorField, SectionProp } from './InspectorField';
import { InspectorRepeater } from './InspectorRepeater';
import { MediaUploader } from '@/components/MediaUploader';
import { SectionStyles, ElementStyle } from '../store/usePageEditorStore';
import { SectionStyles, ElementStyle, PageItem } from '../store/usePageEditorStore';
interface Section {
id: string;
@@ -44,17 +44,6 @@ interface Section {
props: Record<string, SectionProp>;
}
interface PageItem {
id?: number;
type: 'page' | 'template';
cpt?: string;
slug?: string;
title: string;
url?: string;
isSpaLanding?: boolean;
containerWidth?: 'boxed' | 'fullwidth';
}
interface InspectorPanelProps {
page: PageItem | null;
selectedSection: Section | null;

View File

@@ -3,15 +3,7 @@ import { __ } from '@/lib/i18n';
import { cn } from '@/lib/utils';
import { FileText, Layout, Loader2, Home } from 'lucide-react';
interface PageItem {
id?: number;
type: 'page' | 'template';
cpt?: string;
slug?: string;
title: string;
has_template?: boolean;
permalink_base?: string;
}
import { PageItem } from '../store/usePageEditorStore';
interface PageSidebarProps {
pages: PageItem[];

View File

@@ -10,21 +10,7 @@ import { PageSidebar } from './components/PageSidebar';
import { CanvasRenderer } from './components/CanvasRenderer';
import { InspectorPanel } from './components/InspectorPanel';
import { CreatePageModal } from './components/CreatePageModal';
import { usePageEditorStore, Section } from './store/usePageEditorStore';
// Types
interface PageItem {
id?: number;
type: 'page' | 'template';
cpt?: string;
slug?: string;
title: string;
url?: string;
icon?: string;
has_template?: boolean;
permalink_base?: string;
isFrontPage?: boolean;
}
import { usePageEditorStore, Section, PageItem } from './store/usePageEditorStore';
export default function AppearancePages() {
const queryClient = useQueryClient();
@@ -229,12 +215,12 @@ export default function AppearancePages() {
return (
<div className={
cn(
"flex flex-col bg-white transition-all duration-300",
"flex flex-col bg-background transition-all duration-300",
isFullscreen ? "fixed inset-0 z-[100] h-screen" : "h-[calc(100vh-64px)]"
)
} >
{/* Header */}
< div className="flex items-center justify-between px-6 py-3 border-b bg-white" >
< div className="flex items-center justify-between px-6 py-3 border-b bg-background" >
<div>
<h1 className="text-xl font-semibold">{__('Page Editor')}</h1>
<p className="text-sm text-muted-foreground">
@@ -315,7 +301,7 @@ export default function AppearancePages() {
}
/>
) : (
<div className="flex-1 bg-gray-100 flex items-center justify-center text-gray-400">
<div className="flex-1 bg-muted/30 flex items-center justify-center text-muted-foreground">
<div className="text-center">
<Layout className="w-16 h-16 mx-auto mb-4 opacity-50" />
<p className="text-lg">{__('Select a page from the sidebar')}</p>

View File

@@ -60,9 +60,12 @@ export interface PageItem {
slug?: string;
title: string;
url?: string;
icon?: string;
has_template?: boolean;
permalink_base?: string;
isFrontPage?: boolean;
isSpaLanding?: boolean;
containerWidth?: 'boxed' | 'fullwidth';
containerWidth?: 'boxed' | 'fullwidth' | 'default';
}
interface PageEditorState {

View File

@@ -1,4 +1,5 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { Skeleton } from '@/components/ui/skeleton';
@@ -11,6 +12,7 @@ interface DocContentProps {
}
export default function DocContent({ slug }: DocContentProps) {
const navigate = useNavigate();
const [doc, setDoc] = useState<DocContentType | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@@ -139,16 +141,27 @@ export default function DocContent({ slug }: DocContentProps) {
</blockquote>
),
// Links
a: ({ href, children }) => (
<a
href={href}
className="text-primary hover:underline"
target={href?.startsWith('http') ? '_blank' : undefined}
rel={href?.startsWith('http') ? 'noopener noreferrer' : undefined}
>
{children}
</a>
),
a: ({ href, children }) => {
const isExternal = href?.startsWith('http') || href?.startsWith('mailto:');
const isAnchor = href?.startsWith('#');
return (
<a
href={href}
className="text-primary hover:underline cursor-pointer"
target={isExternal ? '_blank' : undefined}
rel={isExternal ? 'noopener noreferrer' : undefined}
onClick={(e) => {
if (!isExternal && !isAnchor && href) {
e.preventDefault();
navigate(href);
}
}}
>
{children}
</a>
);
},
// Lists
ul: ({ children }) => (
<ul className="list-disc pl-6 my-4 space-y-2">{children}</ul>

View File

@@ -0,0 +1,92 @@
import React from 'react';
import { Check, Maximize, Minimize } from 'lucide-react';
interface StepAppearanceProps {
containerWidth: string;
primaryColor: string;
onWidthChange: (width: string) => void;
onColorChange: (color: string) => void;
}
export function StepAppearance({ containerWidth, primaryColor, onWidthChange, onColorChange }: StepAppearanceProps) {
const colors = [
{ name: 'Modern Black', value: '#000000', ring: 'ring-gray-900' },
{ name: 'Trusty Blue', value: '#2563eb', ring: 'ring-blue-600' },
{ name: 'Vibrant Purple', value: '#7c3aed', ring: 'ring-purple-600' },
{ name: 'Forest Green', value: '#16a34a', ring: 'ring-green-600' },
{ name: 'Warm Orange', value: '#ea580c', ring: 'ring-orange-600' },
];
const layouts = [
{
id: 'max-w-6xl',
title: 'Boxed',
description: 'Centered content with whitespace. Best for readability.',
icon: Minimize
// Using Minimize as a proxy for "Contained" visual
},
{
id: 'max-w-full',
title: 'Full Width',
description: 'Edge-to-edge immersive experience.',
icon: Maximize
}
];
return (
<div className="space-y-8 max-w-2xl mx-auto">
<div className="text-center">
<h2 className="text-2xl font-semibold mb-2">Choose your vibe</h2>
<p className="text-muted-foreground">Customize the look and feel of your store.</p>
</div>
<div className="space-y-4">
<h3 className="text-sm font-medium uppercase text-muted-foreground">Layout Style</h3>
<div className="grid grid-cols-2 gap-4">
{layouts.map(layout => {
const isSelected = containerWidth === layout.id;
const Icon = layout.icon;
return (
<button
key={layout.id}
onClick={() => onWidthChange(layout.id)}
className={`p-4 rounded-xl border-2 transition-all hover:bg-accent/50 text-left flex items-start gap-4 ${isSelected ? 'border-primary bg-accent/20' : 'border-border'
}`}
>
<div className={`p-2 rounded-md ${isSelected ? 'bg-primary text-primary-foreground' : 'bg-muted'}`}>
<Icon className="w-5 h-5" />
</div>
<div>
<div className="font-semibold">{layout.title}</div>
<div className="text-xs text-muted-foreground">{layout.description}</div>
</div>
</button>
);
})}
</div>
</div>
<div className="space-y-4">
<h3 className="text-sm font-medium uppercase text-muted-foreground">Brand Color</h3>
<div className="flex flex-wrap gap-4">
{colors.map(color => {
const isSelected = primaryColor === color.value;
return (
<button
key={color.value}
onClick={() => onColorChange(color.value)}
className={`group relative w-16 h-16 rounded-full flex items-center justify-center transition-all ${isSelected ? `ring-4 ${color.ring} ring-offset-2 ring-offset-background scale-110` : 'hover:scale-105'
}`}
style={{ backgroundColor: color.value }}
title={color.name}
>
{isSelected && <Check className="w-6 h-6 text-white drop-shadow-md" />}
</button>
);
})}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,102 @@
import React, { useEffect, useState } from 'react';
import { Sparkles, Home, ChevronRight } from 'lucide-react';
interface StepHomepageProps {
pageId: string | number;
createMagicPage: boolean;
onPageChange: (id: string | number) => void;
onMagicChange: (enabled: boolean) => void;
}
export function StepHomepage({ pageId, createMagicPage, onPageChange, onMagicChange }: StepHomepageProps) {
const [pages, setPages] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Fetch pages for dropdown
const fetchPages = async () => {
try {
const res = await fetch((window as any).WNW_CONFIG?.restUrl + '/pages?per_page=100');
if (res.ok) {
const data = await res.json();
setPages(data);
}
} catch (e) {
console.error('Failed to fetch pages', e);
} finally {
setLoading(false);
}
};
fetchPages();
}, []);
return (
<div className="space-y-6 max-w-2xl mx-auto">
<div className="text-center">
<h2 className="text-2xl font-semibold mb-2">Where should customers land?</h2>
<p className="text-muted-foreground">Choose the entry point for your store.</p>
</div>
<div className="grid gap-6">
{/* Option A: Magic Create */}
<button
onClick={() => {
onMagicChange(true);
onPageChange('');
}}
className={`relative flex items-center gap-4 p-6 rounded-xl border-2 transition-all hover:bg-accent/50 text-left ${createMagicPage ? 'border-primary bg-accent/20' : 'border-border'
}`}
>
<div className={`p-3 rounded-lg ${createMagicPage ? 'bg-primary text-primary-foreground' : 'bg-muted'}`}>
<Sparkles className="w-6 h-6" />
</div>
<div className="flex-1">
<h3 className="font-semibold text-lg">Auto-create "Shop Home"</h3>
<p className="text-sm text-muted-foreground">We'll generate a beautiful homepage for you and set it up automatically.</p>
</div>
{createMagicPage && <div className="w-4 h-4 rounded-full bg-primary" />}
</button>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background px-2 text-muted-foreground">Or select existing</span>
</div>
</div>
{/* Option B: Select Existing */}
<div className={`p-6 rounded-xl border-2 transition-all ${!createMagicPage && pageId ? 'border-primary bg-accent/20' : 'border-border'
}`}>
<div className="flex items-center gap-4 mb-4">
<div className={`p-3 rounded-lg ${!createMagicPage && pageId ? 'bg-primary text-primary-foreground' : 'bg-muted'}`}>
<Home className="w-6 h-6" />
</div>
<div>
<h3 className="font-semibold text-lg">Use an existing page</h3>
<p className="text-sm text-muted-foreground">Select a page you've already created.</p>
</div>
{!createMagicPage && pageId && <div className="w-4 h-4 rounded-full bg-primary ml-auto" />}
</div>
<select
className="w-full h-10 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
value={pageId}
onChange={(e) => {
onMagicChange(false);
onPageChange(e.target.value);
}}
>
<option value="">Select a page...</option>
{pages.map(p => (
<option key={p.id} value={p.id}>
{p.title?.rendered || p.title || p.post_title || `Page #${p.id}`}
</option>
))}
</select>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,66 @@
import React from 'react';
import { LayoutDashboard, ShoppingCart, FileText, Check } from 'lucide-react';
interface StepModeProps {
value: string;
onChange: (mode: string) => void;
}
export function StepMode({ value, onChange }: StepModeProps) {
const modes = [
{
id: 'full',
title: 'Immersive App',
description: 'Your entire store runs as a modern, high-speed app. Best for dedicated e-commerce sites.',
icon: LayoutDashboard,
},
{
id: 'checkout_only',
title: 'Checkout Only',
description: 'Keep your existing theme for pages, but use our super-fast checkout.',
icon: ShoppingCart,
},
{
id: 'disabled',
title: 'Standard',
description: 'Use standard WordPress pages. Good compatibility with legacy plugins.',
icon: FileText,
},
];
return (
<div className="space-y-6">
<div className="text-center">
<h2 className="text-2xl font-semibold mb-2">How do you want to run your store?</h2>
<p className="text-muted-foreground">Select the mode that fits your business needs.</p>
</div>
<div className="grid gap-4 md:grid-cols-3">
{modes.map((mode) => {
const Icon = mode.icon;
const isSelected = value === mode.id;
return (
<button
key={mode.id}
onClick={() => onChange(mode.id)}
className={`relative flex flex-col items-start p-6 rounded-xl border-2 transition-all hover:bg-accent/50 text-left h-full ${isSelected ? 'border-primary bg-accent/20' : 'border-border'
}`}
>
<div className={`p-3 rounded-lg mb-4 ${isSelected ? 'bg-primary text-primary-foreground' : 'bg-muted'}`}>
<Icon className="w-6 h-6" />
</div>
<h3 className="font-semibold text-lg mb-2">{mode.title}</h3>
<p className="text-sm text-muted-foreground">{mode.description}</p>
{isSelected && (
<div className="absolute top-4 right-4 text-primary">
<Check className="w-6 h-6" />
</div>
)}
</button>
);
})}
</div>
</div>
);
}

View File

@@ -0,0 +1,17 @@
import React from 'react';
interface StepProgressProps {
currentStep: number;
totalSteps: number;
}
export function StepProgress({ currentStep, totalSteps }: StepProgressProps) {
return (
<div className="w-full bg-secondary h-2 rounded-full overflow-hidden">
<div
className="h-full bg-primary transition-all duration-500 ease-out"
style={{ width: `${((currentStep + 1) / totalSteps) * 100}%` }}
/>
</div>
);
}

View File

@@ -0,0 +1,159 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { toast } from 'sonner';
import { ArrowRight, ArrowLeft, Check, Loader2, Rocket } from 'lucide-react';
import { StepMode } from './components/StepMode';
import { StepHomepage } from './components/StepHomepage';
import { StepAppearance } from './components/StepAppearance';
import { StepProgress } from './components/StepProgress';
export default function Onboarding() {
const navigate = useNavigate();
const [step, setStep] = useState(0);
const [loading, setLoading] = useState(false);
const [data, setData] = useState({
mode: 'full',
pageId: '',
createMagicPage: false,
containerWidth: 'max-w-6xl',
primaryColor: '#000000',
});
const steps = [
{ component: StepMode, title: 'Mode' },
{ component: StepHomepage, title: 'Homepage' },
{ component: StepAppearance, title: 'Appearance' },
];
const handleNext = async () => {
if (step < steps.length - 1) {
if (step === 1 && !data.createMagicPage && !data.pageId) {
toast.error('Please select a page or choose auto-create');
return;
}
setStep(s => s + 1);
} else {
// Final Submit
setLoading(true);
try {
const payload = {
mode: data.mode,
create_home_page: data.createMagicPage,
entry_page_id: data.pageId,
container_width: data.containerWidth,
primary_color: data.primaryColor
};
const res = await fetch((window as any).WNW_CONFIG?.restUrl + '/onboarding/complete', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': (window as any).WNW_CONFIG?.nonce
},
body: JSON.stringify(payload)
});
const json = await res.json();
if (json.success) {
toast.success('Store setup complete!');
// Update global config to prevent showing onboarding again
if ((window as any).WNW_CONFIG) {
(window as any).WNW_CONFIG.onboardingCompleted = true;
}
navigate('/appearance/pages');
} else {
throw new Error(json.message || 'Setup failed');
}
} catch (e: any) {
toast.error(e.message || 'Something went wrong');
setLoading(false);
}
}
};
const CurrentStepComponent = steps[step].component;
return (
<div className="min-h-screen flex flex-col items-center justify-center p-4 bg-background">
<div className="w-full max-w-4xl bg-card border border-border shadow-xl rounded-2xl overflow-hidden flex flex-col md:flex-row min-h-[600px]">
{/* Sidebar / Info Panel */}
<div className="bg-muted/30 p-8 md:w-1/3 border-b md:border-b-0 md:border-r border-border flex flex-col justify-between">
<div>
<div className="flex items-center gap-2 mb-8 text-primary">
<Rocket className="w-6 h-6" />
<span className="font-bold text-xl tracking-tight">WooNooW Setup</span>
</div>
<div className="space-y-4">
{steps.map((s, i) => (
<div key={i} className={`flex items-center gap-3 ${i === step ? 'text-foreground' : 'text-muted-foreground'}`}>
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium border ${i < step ? 'bg-primary text-primary-foreground border-primary' :
i === step ? 'bg-background border-primary text-primary' :
'bg-muted border-border'
}`}>
{i < step ? <Check className="w-4 h-4" /> : i + 1}
</div>
<span className={i === step ? 'font-medium' : ''}>{s.title}</span>
</div>
))}
</div>
</div>
<div className="mt-8">
<p className="text-xs text-muted-foreground">step {step + 1} of {steps.length}</p>
<div className="mt-2">
<StepProgress currentStep={step} totalSteps={steps.length} />
</div>
</div>
</div>
{/* Main Content Area */}
<div className="flex-1 p-8 flex flex-col">
<div className="flex-1">
<CurrentStepComponent
// Props mapping is dynamic but typed loosely here for simplicity
value={data.mode}
onChange={(val: string) => setData(d => ({ ...d, mode: val }))}
// Homepage props
pageId={data.pageId}
createMagicPage={data.createMagicPage}
onPageChange={(id: string | number) => setData(d => ({ ...d, pageId: String(id) }))}
onMagicChange={(enabled: boolean) => setData(d => ({ ...d, createMagicPage: enabled }))}
// Appearance props
containerWidth={data.containerWidth}
primaryColor={data.primaryColor}
onWidthChange={(w: string) => setData(d => ({ ...d, containerWidth: w }))}
onColorChange={(c: string) => setData(d => ({ ...d, primaryColor: c }))}
/>
</div>
<div className="flex justify-between items-center mt-8 pt-4 border-t border-border">
<button
onClick={() => setStep(s => Math.max(0, s - 1))}
disabled={step === 0 || loading}
className="flex items-center gap-2 px-4 py-2 rounded-md hover:bg-accent disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<ArrowLeft className="w-4 h-4" /> Back
</button>
<button
onClick={handleNext}
disabled={loading}
className="flex items-center gap-2 px-6 py-2 rounded-md bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50 font-medium transition-all shadow-sm"
>
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : null}
{step === steps.length - 1 ? 'Launch Store' : 'Next'}
{!loading && step < steps.length - 1 && <ArrowRight className="w-4 h-4" />}
</button>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,4 +1,6 @@
import React, { useState, useEffect, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { Sparkles } from 'lucide-react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '@/lib/api';
import { SettingsLayout } from './components/SettingsLayout';
@@ -52,6 +54,7 @@ interface StoreSettings {
}
export default function StoreDetailsPage() {
const navigate = useNavigate();
const queryClient = useQueryClient();
const [settings, setSettings] = useState<StoreSettings>({
storeName: '',
@@ -176,7 +179,7 @@ export default function StoreDetailsPage() {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['store-settings'] });
toast.success('Your store details have been updated successfully.');
// Dispatch event to update site title in header
window.dispatchEvent(new CustomEvent('woonoow:store:updated', {
detail: { store_name: settings.storeName }
@@ -203,16 +206,16 @@ export default function StoreDetailsPage() {
const formatted = amount.toFixed(settings.decimals)
.replace('.', settings.decimalSep)
.replace(/\B(?=(\d{3})+(?!\d))/g, settings.thousandSep);
// Get currency symbol from currencies data, fallback to currency code
const currencyInfo = currencies.find((c: any) => c.code === settings.currency);
let symbol = settings.currency; // Default to currency code
if (currencyInfo?.symbol && !currencyInfo.symbol.includes('&#')) {
// Use symbol only if it exists and doesn't contain HTML entities
symbol = currencyInfo.symbol;
}
switch (settings.currencyPosition) {
case 'left':
return `${symbol}${formatted}`;
@@ -233,6 +236,17 @@ export default function StoreDetailsPage() {
description="Manage your store's basic information and regional settings"
onSave={handleSave}
isLoading={isLoading}
action={
<Button
variant="outline"
size="sm"
onClick={() => navigate('/setup')}
className="gap-2"
>
<Sparkles className="w-4 h-4" />
Launch Setup Wizard
</Button>
}
>
{/* Store Overview */}
<div className="bg-primary/10 border border-primary/20 rounded-lg p-4">
@@ -240,7 +254,7 @@ export default function StoreDetailsPage() {
const currencyFlag = flagsData.find((f: any) => f.code === settings.currency);
const currencyInfo = currencies.find((c: any) => c.code === settings.currency);
const countryName = currencyFlag?.country || settings.country;
return (
<>
<p className="text-sm font-medium">
@@ -339,8 +353,8 @@ export default function StoreDetailsPage() {
/>
</SettingsSection>
<SettingsSection
label="Store logo (Dark mode)"
<SettingsSection
label="Store logo (Dark mode)"
description="Optional. If not set, light mode logo will be used in dark mode."
>
<ImageUpload
@@ -480,14 +494,14 @@ export default function StoreDetailsPage() {
onChange={(v) => updateSetting('currency', v)}
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
const displaySymbol = (!currency.symbol || currency.symbol.includes('&#'))
? currency.code
: currency.symbol;
// Find matching flag data and convert to emoji
const flagInfo = flagsData.find((f: any) => f.code === currency.code);
const flagEmoji = flagInfo ? countryCodeToEmoji(flagInfo.countryCode) : '';
return {
value: currency.code,
label: `${flagEmoji} ${currency.name} (${displaySymbol})`.trim(),