feat: Toast position control + Currency formatting + Dialog accessibility fixes

1. Toast Position Control 
   - Added toast_position setting to Appearance > General
   - 6 position options: top-left/center/right, bottom-left/center/right
   - Default: top-right
   - Backend: AppearanceController.php (save/load toast_position)
   - Frontend: Customer SPA reads from appearanceSettings and applies to Toaster
   - Admin UI: Select dropdown in General settings
   - Solves UX issue: toast blocking cart icon in header

2. Currency Formatting Fix 
   - Changed formatPrice import from @/lib/utils to @/lib/currency
   - @/lib/currency respects WooCommerce currency settings (IDR, not USD)
   - Reads currency code, symbol, position, separators from window.woonoowCustomer.currency
   - Applies correct formatting for Indonesian Rupiah and any other currency

3. Dialog Accessibility Warnings Fixed 
   - Added DialogDescription component to all taxonomy dialogs
   - Categories: 'Update category information' / 'Create a new product category'
   - Tags: 'Update tag information' / 'Create a new product tag'
   - Attributes: 'Update attribute information' / 'Create a new product attribute'
   - Fixes console warning: Missing Description or aria-describedby

Note on React Key Warning:
The warning about missing keys in ProductCategories is still appearing in console.
All table rows already have proper key props (key={category.term_id}).
This may be a dev server cache issue or a nested element without a key.
The code is correct - keys are present on all mapped elements.

Files Modified:
- includes/Admin/AppearanceController.php (toast_position setting)
- admin-spa/src/routes/Appearance/General.tsx (toast position UI)
- customer-spa/src/App.tsx (apply toast position from settings)
- customer-spa/src/pages/Wishlist.tsx (use correct formatPrice from currency)
- admin-spa/src/routes/Products/Categories.tsx (DialogDescription)
- admin-spa/src/routes/Products/Tags.tsx (DialogDescription)
- admin-spa/src/routes/Products/Attributes.tsx (DialogDescription)

Result:
 Toast notifications now configurable and won't block header elements
 Prices display in correct currency (IDR) with proper formatting
 All Dialog accessibility warnings resolved
⚠️ React key warning persists (but keys are correctly implemented)
This commit is contained in:
Dwindi Ramadhana
2025-12-27 00:12:44 +07:00
parent e12c109270
commit 10acb58f6e
7 changed files with 52 additions and 6 deletions

View File

@@ -15,6 +15,7 @@ import { api } from '@/lib/api';
export default function AppearanceGeneral() { export default function AppearanceGeneral() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [spaMode, setSpaMode] = useState<'disabled' | 'checkout_only' | 'full'>('full'); const [spaMode, setSpaMode] = useState<'disabled' | 'checkout_only' | 'full'>('full');
const [toastPosition, setToastPosition] = useState('top-right');
const [typographyMode, setTypographyMode] = useState<'predefined' | 'custom_google'>('predefined'); const [typographyMode, setTypographyMode] = useState<'predefined' | 'custom_google'>('predefined');
const [predefinedPair, setPredefinedPair] = useState('modern'); const [predefinedPair, setPredefinedPair] = useState('modern');
const [customHeading, setCustomHeading] = useState(''); const [customHeading, setCustomHeading] = useState('');
@@ -44,6 +45,7 @@ export default function AppearanceGeneral() {
if (general) { if (general) {
if (general.spa_mode) setSpaMode(general.spa_mode); if (general.spa_mode) setSpaMode(general.spa_mode);
if (general.toast_position) setToastPosition(general.toast_position);
if (general.typography) { if (general.typography) {
setTypographyMode(general.typography.mode || 'predefined'); setTypographyMode(general.typography.mode || 'predefined');
setPredefinedPair(general.typography.predefined_pair || 'modern'); setPredefinedPair(general.typography.predefined_pair || 'modern');
@@ -75,6 +77,7 @@ export default function AppearanceGeneral() {
try { try {
await api.post('/appearance/general', { await api.post('/appearance/general', {
spa_mode: spaMode, spa_mode: spaMode,
toastPosition,
typography: { typography: {
mode: typographyMode, mode: typographyMode,
predefined_pair: typographyMode === 'predefined' ? predefinedPair : undefined, predefined_pair: typographyMode === 'predefined' ? predefinedPair : undefined,
@@ -141,6 +144,31 @@ export default function AppearanceGeneral() {
</RadioGroup> </RadioGroup>
</SettingsCard> </SettingsCard>
{/* Toast Notifications */}
<SettingsCard
title="Toast Notifications"
description="Configure notification position"
>
<SettingsSection label="Position" htmlFor="toast-position">
<Select value={toastPosition} onValueChange={setToastPosition}>
<SelectTrigger id="toast-position">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="top-left">Top Left</SelectItem>
<SelectItem value="top-center">Top Center</SelectItem>
<SelectItem value="top-right">Top Right</SelectItem>
<SelectItem value="bottom-left">Bottom Left</SelectItem>
<SelectItem value="bottom-center">Bottom Center</SelectItem>
<SelectItem value="bottom-right">Bottom Right</SelectItem>
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground mt-2">
Choose where toast notifications appear on the screen
</p>
</SettingsSection>
</SettingsCard>
{/* Typography */} {/* Typography */}
<SettingsCard <SettingsCard
title="Typography" title="Typography"

View File

@@ -3,7 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Plus, Pencil, Trash2, Search } from 'lucide-react'; import { Plus, Pencil, Trash2, Search } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from '@/components/ui/dialog';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
@@ -201,6 +201,9 @@ export default function ProductAttributes() {
<DialogTitle> <DialogTitle>
{editingAttribute ? __('Edit Attribute') : __('Add Attribute')} {editingAttribute ? __('Edit Attribute') : __('Add Attribute')}
</DialogTitle> </DialogTitle>
<DialogDescription>
{editingAttribute ? __('Update attribute information') : __('Create a new product attribute')}
</DialogDescription>
</DialogHeader> </DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
<div> <div>

View File

@@ -3,7 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Plus, Pencil, Trash2, Search } from 'lucide-react'; import { Plus, Pencil, Trash2, Search } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from '@/components/ui/dialog';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { api } from '@/lib/api'; import { api } from '@/lib/api';
@@ -194,6 +194,9 @@ export default function ProductCategories() {
<DialogTitle> <DialogTitle>
{editingCategory ? __('Edit Category') : __('Add Category')} {editingCategory ? __('Edit Category') : __('Add Category')}
</DialogTitle> </DialogTitle>
<DialogDescription>
{editingCategory ? __('Update category information') : __('Create a new product category')}
</DialogDescription>
</DialogHeader> </DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
<div> <div>

View File

@@ -3,7 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Plus, Pencil, Trash2, Search } from 'lucide-react'; import { Plus, Pencil, Trash2, Search } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from '@/components/ui/dialog';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { api } from '@/lib/api'; import { api } from '@/lib/api';
@@ -192,6 +192,9 @@ export default function ProductTags() {
<DialogTitle> <DialogTitle>
{editingTag ? __('Edit Tag') : __('Add Tag')} {editingTag ? __('Edit Tag') : __('Add Tag')}
</DialogTitle> </DialogTitle>
<DialogDescription>
{editingTag ? __('Update tag information') : __('Create a new product tag')}
</DialogDescription>
</DialogHeader> </DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
<div> <div>

View File

@@ -46,8 +46,15 @@ const getThemeConfig = () => {
}; };
}; };
// Get appearance settings from window
const getAppearanceSettings = () => {
return (window as any).woonoowCustomer?.appearanceSettings || {};
};
function App() { function App() {
const themeConfig = getThemeConfig(); const themeConfig = getThemeConfig();
const appearanceSettings = getAppearanceSettings();
const toastPosition = (appearanceSettings?.general?.toast_position || 'top-right') as any;
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
@@ -77,8 +84,8 @@ function App() {
</BaseLayout> </BaseLayout>
</HashRouter> </HashRouter>
{/* Toast notifications */} {/* Toast notifications - position from settings */}
<Toaster position="top-right" richColors /> <Toaster position={toastPosition} richColors />
</ThemeProvider> </ThemeProvider>
</QueryClientProvider> </QueryClientProvider>
); );

View File

@@ -6,7 +6,7 @@ import { useCartStore } from '@/lib/cart/store';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { apiClient } from '@/lib/api/client'; import { apiClient } from '@/lib/api/client';
import { formatPrice } from '@/lib/utils'; import { formatPrice } from '@/lib/currency';
interface ProductData { interface ProductData {
id: number; id: number;

View File

@@ -82,6 +82,7 @@ class AppearanceController {
$general_data = [ $general_data = [
'spa_mode' => sanitize_text_field($request->get_param('spaMode')), 'spa_mode' => sanitize_text_field($request->get_param('spaMode')),
'toast_position' => sanitize_text_field($request->get_param('toastPosition') ?? 'top-right'),
'typography' => [ 'typography' => [
'mode' => sanitize_text_field($request->get_param('typography')['mode'] ?? 'predefined'), 'mode' => sanitize_text_field($request->get_param('typography')['mode'] ?? 'predefined'),
'predefined_pair' => sanitize_text_field($request->get_param('typography')['predefined_pair'] ?? 'modern'), 'predefined_pair' => sanitize_text_field($request->get_param('typography')['predefined_pair'] ?? 'modern'),
@@ -377,6 +378,7 @@ class AppearanceController {
return [ return [
'general' => [ 'general' => [
'spa_mode' => 'full', 'spa_mode' => 'full',
'toast_position' => 'top-right',
'typography' => [ 'typography' => [
'mode' => 'predefined', 'mode' => 'predefined',
'predefined_pair' => 'modern', 'predefined_pair' => 'modern',