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:
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user