Improve Features page ordering and mobile UI consistency
- Fix module ordering with local state tracking for immediate UI updates - Add tab centering when selected (scroll to center) - Use finally block in handleApplyOrder to ensure state reset - Add cancelOrder translation key - Increase order card min-width for better readability - Normalize mobile top bar height with min-height constraint - Add display:flex to mobile title sections for proper layout 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { useTranslation } from '../../contexts/LanguageContext';
|
||||
import { useSidebar } from '../../contexts/SidebarContext';
|
||||
@@ -7,7 +7,7 @@ import type { ModuleId } from '../../contexts/ModulesContext';
|
||||
import Feature1Tab from '../../components/admin/Feature1Tab';
|
||||
import '../../styles/AdminPanel.css';
|
||||
|
||||
type TabId = 'config' | 'feature1' | 'feature2' | 'feature3' | 'search' | 'notifications';
|
||||
type TabId = 'config' | 'dashboard' | 'feature1' | 'feature2' | 'feature3' | 'search' | 'notifications';
|
||||
|
||||
export default function Features() {
|
||||
const { user: currentUser } = useAuth();
|
||||
@@ -19,15 +19,64 @@ export default function Features() {
|
||||
const saveRef = useRef(saveModulesToBackend);
|
||||
const [draggedItem, setDraggedItem] = useState<string | null>(null);
|
||||
const [localOrder, setLocalOrder] = useState<string[]>([]);
|
||||
const [localPositions, setLocalPositions] = useState<Record<string, 'top' | 'bottom'>>({});
|
||||
const [hasOrderChanges, setHasOrderChanges] = useState(false);
|
||||
const tabsContainerRef = useRef<HTMLDivElement>(null);
|
||||
const isUserEditing = useRef(false); // Track if user is actively editing
|
||||
|
||||
// Sync local order with context order
|
||||
// Sync local order with context order (only when moduleOrder changes, not moduleStates)
|
||||
useEffect(() => {
|
||||
if (moduleOrder.length > 0) {
|
||||
setLocalOrder(moduleOrder);
|
||||
if (moduleOrder.length > 0 && !isUserEditing.current) {
|
||||
// Start with current order
|
||||
const fullOrder = [...moduleOrder];
|
||||
|
||||
// Insert missing modules at their default positions from TOGGLEABLE_MODULES
|
||||
TOGGLEABLE_MODULES.forEach((module, defaultIndex) => {
|
||||
if (!fullOrder.includes(module.id)) {
|
||||
// Insert at the default index position, or at end if index is beyond current length
|
||||
const insertAt = Math.min(defaultIndex, fullOrder.length);
|
||||
fullOrder.splice(insertAt, 0, module.id);
|
||||
}
|
||||
});
|
||||
|
||||
setLocalOrder(fullOrder);
|
||||
setHasOrderChanges(false);
|
||||
}
|
||||
}, [moduleOrder]);
|
||||
|
||||
// Sync positions from moduleStates only on initial load or when not editing
|
||||
useEffect(() => {
|
||||
if (!isUserEditing.current) {
|
||||
const positions: Record<string, 'top' | 'bottom'> = {};
|
||||
TOGGLEABLE_MODULES.forEach(module => {
|
||||
const state = moduleStates[module.id];
|
||||
positions[module.id] = state?.position || module.defaultPosition;
|
||||
});
|
||||
setLocalPositions(positions);
|
||||
}
|
||||
}, [moduleStates]);
|
||||
|
||||
// Scroll active tab to center of container
|
||||
const scrollActiveTabIntoView = useCallback((tabId: string) => {
|
||||
if (tabsContainerRef.current) {
|
||||
const container = tabsContainerRef.current;
|
||||
const activeButton = container.querySelector(`[data-tab-id="${tabId}"]`) as HTMLElement;
|
||||
if (activeButton) {
|
||||
const containerWidth = container.clientWidth;
|
||||
const buttonLeft = activeButton.offsetLeft;
|
||||
const buttonWidth = activeButton.offsetWidth;
|
||||
// Calculate scroll position to center the button
|
||||
const scrollLeft = buttonLeft - (containerWidth / 2) + (buttonWidth / 2);
|
||||
container.scrollTo({ left: Math.max(0, scrollLeft), behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Handle tab change with scroll
|
||||
const handleTabChange = useCallback((tabId: TabId) => {
|
||||
setActiveTab(tabId);
|
||||
setTimeout(() => scrollActiveTabIntoView(tabId), 50);
|
||||
}, [scrollActiveTabIntoView]);
|
||||
// Keep saveRef updated with latest function
|
||||
useEffect(() => {
|
||||
saveRef.current = saveModulesToBackend;
|
||||
@@ -133,24 +182,32 @@ export default function Features() {
|
||||
e.stopPropagation();
|
||||
if (!draggedItem) return;
|
||||
|
||||
const draggedPosition = moduleStates[draggedItem as ModuleId]?.position || 'top';
|
||||
const draggedPosition = localPositions[draggedItem] || 'top';
|
||||
|
||||
// If dropping on same item, just change section if different
|
||||
if (draggedItem === targetModuleId) {
|
||||
if (draggedPosition !== targetSection) {
|
||||
isUserEditing.current = true; // Mark as user editing
|
||||
hasUserMadeChanges.current = true;
|
||||
setModulePosition(draggedItem as ModuleId, targetSection);
|
||||
// Update local positions immediately for UI responsiveness
|
||||
setLocalPositions(prev => ({ ...prev, [draggedItem]: targetSection }));
|
||||
setHasOrderChanges(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Change position if moving to different section
|
||||
if (draggedPosition !== targetSection) {
|
||||
isUserEditing.current = true; // Mark as user editing
|
||||
hasUserMadeChanges.current = true;
|
||||
setModulePosition(draggedItem as ModuleId, targetSection);
|
||||
// Update local positions immediately for UI responsiveness
|
||||
setLocalPositions(prev => ({ ...prev, [draggedItem]: targetSection }));
|
||||
}
|
||||
|
||||
// Reorder within the list
|
||||
isUserEditing.current = true; // Mark as user editing
|
||||
const newOrder = [...localOrder];
|
||||
const draggedIndex = newOrder.indexOf(draggedItem);
|
||||
const targetIndex = newOrder.indexOf(targetModuleId);
|
||||
@@ -168,12 +225,16 @@ export default function Features() {
|
||||
e.preventDefault();
|
||||
if (!draggedItem) return;
|
||||
|
||||
const draggedPosition = moduleStates[draggedItem as ModuleId]?.position || 'top';
|
||||
const draggedPosition = localPositions[draggedItem] || 'top';
|
||||
|
||||
// Change position if moving to different section
|
||||
if (draggedPosition !== section) {
|
||||
isUserEditing.current = true; // Mark as user editing
|
||||
hasUserMadeChanges.current = true;
|
||||
setModulePosition(draggedItem as ModuleId, section);
|
||||
// Update local positions immediately for UI responsiveness
|
||||
setLocalPositions(prev => ({ ...prev, [draggedItem]: section }));
|
||||
setHasOrderChanges(true);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -181,26 +242,41 @@ export default function Features() {
|
||||
try {
|
||||
setModuleOrder(localOrder);
|
||||
await saveModuleOrder(localOrder);
|
||||
setHasOrderChanges(false);
|
||||
} catch (error) {
|
||||
console.error('Failed to save order:', error);
|
||||
} finally {
|
||||
isUserEditing.current = false;
|
||||
setHasOrderChanges(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelOrder = () => {
|
||||
isUserEditing.current = false; // Done editing
|
||||
setLocalOrder(moduleOrder);
|
||||
// Reset positions from moduleStates
|
||||
const positions: Record<string, 'top' | 'bottom'> = {};
|
||||
TOGGLEABLE_MODULES.forEach(module => {
|
||||
const state = moduleStates[module.id];
|
||||
positions[module.id] = state?.position || module.defaultPosition;
|
||||
});
|
||||
setLocalPositions(positions);
|
||||
setHasOrderChanges(false);
|
||||
};
|
||||
|
||||
const getModuleInfo = (moduleId: string) => {
|
||||
const module = TOGGLEABLE_MODULES.find(m => m.id === moduleId);
|
||||
return module || { id: moduleId, icon: 'extension', defaultEnabled: true };
|
||||
};
|
||||
|
||||
// Split modules by position for the config tab
|
||||
// Split modules by position for the config tab (using localPositions for immediate UI updates)
|
||||
const topOrderModules = localOrder.filter(id => {
|
||||
const state = moduleStates[id as ModuleId];
|
||||
return !state || state.position === 'top';
|
||||
const position = localPositions[id];
|
||||
return !position || position === 'top';
|
||||
});
|
||||
|
||||
const bottomOrderModules = localOrder.filter(id => {
|
||||
const state = moduleStates[id as ModuleId];
|
||||
return state && state.position === 'bottom';
|
||||
const position = localPositions[id];
|
||||
return position === 'bottom';
|
||||
});
|
||||
|
||||
const renderConfigTab = () => {
|
||||
@@ -233,7 +309,6 @@ export default function Features() {
|
||||
</div>
|
||||
<div className="order-card-info">
|
||||
<span className="order-card-name">{moduleName}</span>
|
||||
<span className="order-card-desc">{t.featuresPage?.orderDesc || 'Trascina per riordinare'}</span>
|
||||
</div>
|
||||
<div className="order-card-handle">
|
||||
<span className="material-symbols-outlined">drag_indicator</span>
|
||||
@@ -274,7 +349,6 @@ export default function Features() {
|
||||
</div>
|
||||
<div className="order-card-info">
|
||||
<span className="order-card-name">{moduleName}</span>
|
||||
<span className="order-card-desc">{t.featuresPage?.orderDesc || 'Trascina per riordinare'}</span>
|
||||
</div>
|
||||
<div className="order-card-handle">
|
||||
<span className="material-symbols-outlined">drag_indicator</span>
|
||||
@@ -290,6 +364,10 @@ export default function Features() {
|
||||
|
||||
{hasOrderChanges && (
|
||||
<div className="order-actions">
|
||||
<button className="btn-secondary" onClick={handleCancelOrder}>
|
||||
<span className="material-symbols-outlined">close</span>
|
||||
{t.featuresPage?.cancelOrder || 'Annulla'}
|
||||
</button>
|
||||
<button className="btn-primary" onClick={handleApplyOrder}>
|
||||
<span className="material-symbols-outlined">check</span>
|
||||
{t.featuresPage?.applyOrder || 'Applica'}
|
||||
@@ -307,6 +385,19 @@ export default function Features() {
|
||||
switch (activeTab) {
|
||||
case 'config':
|
||||
return renderConfigTab();
|
||||
case 'dashboard':
|
||||
return (
|
||||
<>
|
||||
{renderModuleToggle('dashboard')}
|
||||
<div className="tab-content-placeholder">
|
||||
<div className="placeholder-icon">
|
||||
<span className="material-symbols-outlined">dashboard</span>
|
||||
</div>
|
||||
<h3>{t.sidebar.dashboard}</h3>
|
||||
<p>{t.features.comingSoon}</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
case 'feature1':
|
||||
return (
|
||||
<>
|
||||
@@ -374,7 +465,7 @@ export default function Features() {
|
||||
return (
|
||||
<main className="main-content admin-panel-root">
|
||||
<div className="page-tabs-container">
|
||||
<div className="page-tabs-slider">
|
||||
<div className="page-tabs-slider" ref={tabsContainerRef}>
|
||||
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
|
||||
<span className="material-symbols-outlined">menu</span>
|
||||
</button>
|
||||
@@ -384,21 +475,24 @@ export default function Features() {
|
||||
</div>
|
||||
<div className="page-tabs-divider"></div>
|
||||
<button
|
||||
data-tab-id="config"
|
||||
className={`page-tab-btn ${activeTab === 'config' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('config')}
|
||||
onClick={() => handleTabChange('config')}
|
||||
>
|
||||
<span className="material-symbols-outlined">tune</span>
|
||||
<span>{t.featuresPage?.configTab || 'Configurazione'}</span>
|
||||
</button>
|
||||
{TOGGLEABLE_MODULES.map((module) => {
|
||||
const moduleName = t.sidebar[module.id as keyof typeof t.sidebar] || module.id;
|
||||
{[...topOrderModules, ...bottomOrderModules].map((moduleId) => {
|
||||
const moduleInfo = getModuleInfo(moduleId);
|
||||
const moduleName = t.sidebar[moduleId as keyof typeof t.sidebar] || moduleId;
|
||||
return (
|
||||
<button
|
||||
key={module.id}
|
||||
className={`page-tab-btn ${activeTab === module.id ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab(module.id as TabId)}
|
||||
key={moduleId}
|
||||
data-tab-id={moduleId}
|
||||
className={`page-tab-btn ${activeTab === moduleId ? 'active' : ''}`}
|
||||
onClick={() => handleTabChange(moduleId as TabId)}
|
||||
>
|
||||
<span className="material-symbols-outlined">{module.icon}</span>
|
||||
<span className="material-symbols-outlined">{moduleInfo.icon}</span>
|
||||
<span>{moduleName}</span>
|
||||
</button>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user