Initial commit
This commit is contained in:
217
frontend/src/pages/admin/Features.tsx
Normal file
217
frontend/src/pages/admin/Features.tsx
Normal file
@@ -0,0 +1,217 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { useTranslation } from '../../contexts/LanguageContext';
|
||||
import { useSidebar } from '../../contexts/SidebarContext';
|
||||
import { useModules } from '../../contexts/ModulesContext';
|
||||
import type { ModuleId } from '../../contexts/ModulesContext';
|
||||
import Feature1Tab from '../../components/admin/Feature1Tab';
|
||||
import '../../styles/AdminPanel.css';
|
||||
|
||||
type TabId = 'feature1' | 'feature2' | 'feature3';
|
||||
|
||||
export default function Features() {
|
||||
const { user: currentUser } = useAuth();
|
||||
const { t } = useTranslation();
|
||||
const { toggleMobileMenu } = useSidebar();
|
||||
const { moduleStates, setModuleEnabled, saveModulesToBackend, hasInitialized } = useModules();
|
||||
const [activeTab, setActiveTab] = useState<TabId>('feature1');
|
||||
const hasUserMadeChanges = useRef(false);
|
||||
const saveRef = useRef(saveModulesToBackend);
|
||||
const [tooltip, setTooltip] = useState<{ text: string; left: number; visible: boolean }>({
|
||||
text: '',
|
||||
left: 0,
|
||||
visible: false,
|
||||
});
|
||||
|
||||
const handleTabMouseEnter = (text: string, e: React.MouseEvent) => {
|
||||
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
||||
setTooltip({
|
||||
text,
|
||||
left: rect.left + rect.width / 2,
|
||||
visible: true,
|
||||
});
|
||||
};
|
||||
|
||||
const handleTabMouseLeave = () => {
|
||||
setTooltip((prev) => ({ ...prev, visible: false }));
|
||||
};
|
||||
|
||||
// Keep saveRef updated with latest function
|
||||
useEffect(() => {
|
||||
saveRef.current = saveModulesToBackend;
|
||||
}, [saveModulesToBackend]);
|
||||
|
||||
if (!currentUser?.is_superuser) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleModuleToggle = async (moduleId: ModuleId, type: 'admin' | 'user', enabled: boolean) => {
|
||||
hasUserMadeChanges.current = true;
|
||||
setModuleEnabled(moduleId, type, enabled);
|
||||
};
|
||||
|
||||
// Save changes when moduleStates change, but debounce to avoid too many requests
|
||||
// Only save if: 1) Backend data has been loaded, and 2) User has made changes
|
||||
useEffect(() => {
|
||||
if (!hasInitialized || !hasUserMadeChanges.current) {
|
||||
return;
|
||||
}
|
||||
const timeoutId = setTimeout(() => {
|
||||
saveRef.current().catch(console.error);
|
||||
}, 300);
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [moduleStates, hasInitialized]);
|
||||
|
||||
// Save on unmount if there are pending changes (empty deps = only on unmount)
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (hasUserMadeChanges.current) {
|
||||
saveRef.current().catch(console.error);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const getModuleDescription = (moduleId: string): string => {
|
||||
const key = `${moduleId}Desc` as keyof typeof t.admin;
|
||||
return t.admin[key] || t.admin.moduleDefaultDesc;
|
||||
};
|
||||
|
||||
const renderModuleToggle = (moduleId: ModuleId) => {
|
||||
const state = moduleStates[moduleId];
|
||||
const adminEnabled = state?.admin ?? true;
|
||||
const userEnabled = state?.user ?? true;
|
||||
|
||||
return (
|
||||
<div className="feature-header">
|
||||
<div className="feature-header-info">
|
||||
<p>{getModuleDescription(moduleId)}</p>
|
||||
</div>
|
||||
<div className="feature-header-actions">
|
||||
<div className={`feature-status-badge ${adminEnabled ? 'active' : ''}`}>
|
||||
{adminEnabled ? t.admin.active : t.admin.inactive}
|
||||
</div>
|
||||
<div className="toggle-group">
|
||||
<span className="toggle-label">{t.admin.adminRole}</span>
|
||||
<label className="toggle-modern">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={adminEnabled}
|
||||
onChange={(e) => handleModuleToggle(moduleId, 'admin', e.target.checked)}
|
||||
/>
|
||||
<span className="toggle-slider-modern"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="toggle-group">
|
||||
<span className="toggle-label">{t.admin.userRole}</span>
|
||||
<label className={`toggle-modern ${!adminEnabled ? 'disabled' : ''}`}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={userEnabled}
|
||||
disabled={!adminEnabled}
|
||||
onChange={(e) => handleModuleToggle(moduleId, 'user', e.target.checked)}
|
||||
/>
|
||||
<span className="toggle-slider-modern"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderTabContent = () => {
|
||||
switch (activeTab) {
|
||||
case 'feature1':
|
||||
return (
|
||||
<>
|
||||
{renderModuleToggle('feature1')}
|
||||
<Feature1Tab />
|
||||
</>
|
||||
);
|
||||
case 'feature2':
|
||||
return (
|
||||
<>
|
||||
{renderModuleToggle('feature2')}
|
||||
<div className="tab-content-placeholder">
|
||||
<div className="placeholder-icon">
|
||||
<span className="material-symbols-outlined">download</span>
|
||||
</div>
|
||||
<h3>{t.features.feature2}</h3>
|
||||
<p>{t.features.comingSoon}</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
case 'feature3':
|
||||
return (
|
||||
<>
|
||||
{renderModuleToggle('feature3')}
|
||||
<div className="tab-content-placeholder">
|
||||
<div className="placeholder-icon">
|
||||
<span className="material-symbols-outlined">cast</span>
|
||||
</div>
|
||||
<h3>{t.features.feature3}</h3>
|
||||
<p>{t.features.comingSoon}</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="main-content admin-panel-root">
|
||||
{/* Tab Tooltip */}
|
||||
{tooltip.visible && (
|
||||
<div
|
||||
className="admin-tab-tooltip"
|
||||
style={{ left: tooltip.left }}
|
||||
>
|
||||
{tooltip.text}
|
||||
</div>
|
||||
)}
|
||||
<div className="admin-tabs-container">
|
||||
<div className="admin-tabs-slider">
|
||||
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
|
||||
<span className="material-symbols-outlined">menu</span>
|
||||
</button>
|
||||
<div className="admin-title-section">
|
||||
<span className="material-symbols-outlined">extension</span>
|
||||
<span className="admin-title-text">{t.featuresPage.title}</span>
|
||||
</div>
|
||||
<div className="admin-tabs-divider"></div>
|
||||
<button
|
||||
className={`admin-tab-btn ${activeTab === 'feature1' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('feature1')}
|
||||
onMouseEnter={(e) => handleTabMouseEnter(t.sidebar.feature1, e)}
|
||||
onMouseLeave={handleTabMouseLeave}
|
||||
>
|
||||
<span className="material-symbols-outlined">playlist_play</span>
|
||||
<span>{t.sidebar.feature1}</span>
|
||||
</button>
|
||||
<button
|
||||
className={`admin-tab-btn ${activeTab === 'feature2' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('feature2')}
|
||||
onMouseEnter={(e) => handleTabMouseEnter(t.sidebar.feature2, e)}
|
||||
onMouseLeave={handleTabMouseLeave}
|
||||
>
|
||||
<span className="material-symbols-outlined">download</span>
|
||||
<span>{t.sidebar.feature2}</span>
|
||||
</button>
|
||||
<button
|
||||
className={`admin-tab-btn ${activeTab === 'feature3' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('feature3')}
|
||||
onMouseEnter={(e) => handleTabMouseEnter(t.sidebar.feature3, e)}
|
||||
onMouseLeave={handleTabMouseLeave}
|
||||
>
|
||||
<span className="material-symbols-outlined">cast</span>
|
||||
<span>{t.sidebar.feature3}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="admin-tab-content">
|
||||
{renderTabContent()}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user