deduplicate course tab logic, add pure useCourseTabsState, and sync tab state with URL

This commit is contained in:
austinkelsay 2025-05-12 09:50:33 -05:00
parent 5a79523ba0
commit ccda05df96
No known key found for this signature in database
GPG Key ID: 5A763922E5BA08EE
4 changed files with 205 additions and 166 deletions

View File

@ -3,11 +3,15 @@ import useCourseTabs from './useCourseTabs';
import useCoursePayment from './useCoursePayment'; import useCoursePayment from './useCoursePayment';
import useCourseData from './useCourseData'; import useCourseData from './useCourseData';
import useLessons from './useLessons'; import useLessons from './useLessons';
import useCourseNavigation from './useCourseNavigation';
import useCourseTabsState from './useCourseTabsState';
export { export {
useCourseDecryption, useCourseDecryption,
useCourseTabs, useCourseTabs,
useCoursePayment, useCoursePayment,
useCourseData, useCourseData,
useLessons useLessons,
useCourseNavigation,
useCourseTabsState
}; };

View File

@ -1,4 +1,5 @@
import { useState, useEffect, useMemo } from 'react'; import { useState, useEffect, useCallback } from 'react';
import useCourseTabsState from './useCourseTabsState';
/** /**
* Hook to manage course navigation and tab logic * Hook to manage course navigation and tab logic
@ -8,19 +9,20 @@ import { useState, useEffect, useMemo } from 'react';
*/ */
const useCourseNavigation = (router, isMobileView) => { const useCourseNavigation = (router, isMobileView) => {
const [activeIndex, setActiveIndex] = useState(0); const [activeIndex, setActiveIndex] = useState(0);
const [sidebarVisible, setSidebarVisible] = useState(false);
const [activeTab, setActiveTab] = useState('overview'); // Default to overview tab // Use the base hook for core tab state functionality
const {
// Memoized function to get the tab map based on view mode activeTab,
const tabMap = useMemo(() => { setActiveTab,
const baseTabMap = ['overview', 'content', 'qa']; sidebarVisible,
if (isMobileView) { setSidebarVisible,
const mobileTabMap = [...baseTabMap]; tabMap,
mobileTabMap.splice(2, 0, 'lessons'); getActiveTabIndex,
return mobileTabMap; getTabItems,
} toggleSidebar: baseToggleSidebar
return baseTabMap; } = useCourseTabsState({
}, [isMobileView]); isMobileView
});
// Initialize navigation state based on router // Initialize navigation state based on router
useEffect(() => { useEffect(() => {
@ -39,10 +41,10 @@ const useCourseNavigation = (router, isMobileView) => {
// Auto-open sidebar on desktop, close on mobile // Auto-open sidebar on desktop, close on mobile
setSidebarVisible(!isMobileView); setSidebarVisible(!isMobileView);
} }
}, [router.isReady, router.query, isMobileView]); }, [router.isReady, router.query, isMobileView, setActiveTab, setSidebarVisible]);
// Function to handle lesson selection // Function to handle lesson selection
const handleLessonSelect = (index) => { const handleLessonSelect = useCallback((index) => {
setActiveIndex(index); setActiveIndex(index);
// Update URL without causing a page reload (for bookmarking purposes) // Update URL without causing a page reload (for bookmarking purposes)
@ -54,10 +56,10 @@ const useCourseNavigation = (router, isMobileView) => {
setActiveTab('content'); setActiveTab('content');
setSidebarVisible(false); setSidebarVisible(false);
} }
}; }, [router.query.slug, isMobileView, setActiveTab, setSidebarVisible]);
// Function to toggle tab // Function to toggle tab with lesson state integration
const toggleTab = (index) => { const toggleTab = useCallback((index) => {
const tabName = tabMap[index]; const tabName = tabMap[index];
setActiveTab(tabName); setActiveTab(tabName);
@ -65,66 +67,7 @@ const useCourseNavigation = (router, isMobileView) => {
if (isMobileView) { if (isMobileView) {
setSidebarVisible(tabName === 'lessons'); setSidebarVisible(tabName === 'lessons');
} }
}; }, [tabMap, isMobileView, setActiveTab, setSidebarVisible]);
// Function to toggle sidebar visibility
const toggleSidebar = () => {
setSidebarVisible(!sidebarVisible);
};
// Map active tab name back to index for MenuTab
const getActiveTabIndex = () => {
return tabMap.indexOf(activeTab);
};
// Create tab items for MenuTab
const getTabItems = () => {
const items = [
{
label: 'Overview',
icon: 'pi pi-home',
},
{
label: 'Content',
icon: 'pi pi-book',
}
];
// Add lessons tab only on mobile
if (isMobileView) {
items.push({
label: 'Lessons',
icon: 'pi pi-list',
});
}
items.push({
label: 'Comments',
icon: 'pi pi-comments',
});
return items;
};
// Add keyboard navigation support for tabs
useEffect(() => {
const handleKeyDown = (e) => {
if (e.key === 'ArrowRight') {
const currentIndex = getActiveTabIndex();
const nextIndex = (currentIndex + 1) % tabMap.length;
toggleTab(nextIndex);
} else if (e.key === 'ArrowLeft') {
const currentIndex = getActiveTabIndex();
const prevIndex = (currentIndex - 1 + tabMap.length) % tabMap.length;
toggleTab(prevIndex);
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [activeTab, tabMap]);
return { return {
activeIndex, activeIndex,
@ -135,7 +78,7 @@ const useCourseNavigation = (router, isMobileView) => {
setSidebarVisible, setSidebarVisible,
handleLessonSelect, handleLessonSelect,
toggleTab, toggleTab,
toggleSidebar, toggleSidebar: baseToggleSidebar,
getActiveTabIndex, getActiveTabIndex,
getTabItems, getTabItems,
tabMap tabMap

View File

@ -1,8 +1,10 @@
import { useState, useEffect, useMemo, useCallback } from 'react'; import { useEffect, useCallback } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import useWindowWidth from '../useWindowWidth'; import useWindowWidth from '../useWindowWidth';
import useCourseTabsState from './useCourseTabsState';
/** /**
* @deprecated Use useCourseTabsState for pure state or useCourseNavigation for router integration
* Hook to manage course tabs, navigation, and sidebar visibility * Hook to manage course tabs, navigation, and sidebar visibility
* @param {Object} options - Configuration options * @param {Object} options - Configuration options
* @param {Array} options.tabMap - Optional custom tab map to use * @param {Array} options.tabMap - Optional custom tab map to use
@ -13,49 +15,42 @@ const useCourseTabs = (options = {}) => {
const router = useRouter(); const router = useRouter();
const windowWidth = useWindowWidth(); const windowWidth = useWindowWidth();
const isMobileView = typeof windowWidth === 'number' ? windowWidth <= 968 : false; const isMobileView = typeof windowWidth === 'number' ? windowWidth <= 968 : false;
// Tab management state
const [activeTab, setActiveTab] = useState('overview');
const [sidebarVisible, setSidebarVisible] = useState(
options.initialSidebarVisible !== undefined ? options.initialSidebarVisible : !isMobileView
);
// Get tab map based on view mode // Use the base hook for core tab state functionality
const tabMap = useMemo(() => { const {
const baseTabMap = options.tabMap || ['overview', 'content', 'qa']; activeTab,
if (isMobileView) { setActiveTab,
const mobileTabMap = [...baseTabMap]; sidebarVisible,
// Insert lessons tab before qa in mobile view setSidebarVisible,
if (!mobileTabMap.includes('lessons')) { tabMap,
mobileTabMap.splice(2, 0, 'lessons'); getActiveTabIndex,
} getTabItems,
return mobileTabMap; toggleSidebar
} } = useCourseTabsState({
return baseTabMap; tabMap: options.tabMap,
}, [isMobileView, options.tabMap]); initialSidebarVisible: options.initialSidebarVisible,
isMobileView
});
// Update tabs and sidebar based on router query // Update tabs and sidebar based on router query
useEffect(() => { useEffect(() => {
if (router.isReady) { if (router.isReady) {
const { active } = router.query; const { active, tab } = router.query;
if (active !== undefined) {
// If tab is specified in the URL, use that
if (tab && tabMap.includes(tab)) {
setActiveTab(tab);
} else if (active !== undefined) {
// If we have an active lesson, switch to content tab // If we have an active lesson, switch to content tab
setActiveTab('content'); setActiveTab('content');
} else { } else {
// Default to overview tab when no active parameter // Default to overview tab when no parameters
setActiveTab('overview'); setActiveTab('overview');
} }
// Auto-open sidebar on desktop, close on mobile
setSidebarVisible(!isMobileView);
} }
}, [router.isReady, router.query, isMobileView]); }, [router.isReady, router.query, tabMap, setActiveTab]);
// Get active tab index // Toggle between tabs with router integration
const getActiveTabIndex = useCallback(() => {
return tabMap.indexOf(activeTab);
}, [activeTab, tabMap]);
// Toggle between tabs
const toggleTab = useCallback((indexOrName) => { const toggleTab = useCallback((indexOrName) => {
const tabName = typeof indexOrName === 'number' const tabName = typeof indexOrName === 'number'
? tabMap[indexOrName] ? tabMap[indexOrName]
@ -67,61 +62,18 @@ const useCourseTabs = (options = {}) => {
if (isMobileView) { if (isMobileView) {
setSidebarVisible(tabName === 'lessons'); setSidebarVisible(tabName === 'lessons');
} }
}, [tabMap, isMobileView]);
// Toggle sidebar visibility
const toggleSidebar = useCallback(() => {
setSidebarVisible(prev => !prev);
}, []);
// Generate tab items for MenuTab component
const getTabItems = useCallback(() => {
const items = [
{
label: 'Overview',
icon: 'pi pi-home',
},
{
label: 'Content',
icon: 'pi pi-book',
}
];
// Add lessons tab only on mobile // Sync URL with tab change using shallow routing
if (isMobileView) { const newQuery = {
items.push({ ...router.query,
label: 'Lessons', tab: tabName === 'overview' ? undefined : tabName
icon: 'pi pi-list',
});
}
items.push({
label: 'Comments',
icon: 'pi pi-comments',
});
return items;
}, [isMobileView]);
// Setup keyboard navigation for tabs
useEffect(() => {
const handleKeyDown = (e) => {
if (e.key === 'ArrowRight') {
const currentIndex = getActiveTabIndex();
const nextIndex = (currentIndex + 1) % tabMap.length;
toggleTab(nextIndex);
} else if (e.key === 'ArrowLeft') {
const currentIndex = getActiveTabIndex();
const prevIndex = (currentIndex - 1 + tabMap.length) % tabMap.length;
toggleTab(prevIndex);
}
}; };
router.push(
document.addEventListener('keydown', handleKeyDown); { pathname: router.pathname, query: newQuery },
return () => { undefined,
document.removeEventListener('keydown', handleKeyDown); { shallow: true }
}; );
}, [getActiveTabIndex, tabMap, toggleTab]); }, [tabMap, isMobileView, router, setActiveTab, setSidebarVisible]);
return { return {
activeTab, activeTab,

View File

@ -0,0 +1,140 @@
import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
/**
* Base hook for tab state management with no router or side-effects
* This pure hook manages the tab state and sidebar visibility
*
* @param {Object} options - Configuration options
* @param {Array} options.tabMap - Optional custom tab map to use
* @param {boolean} options.initialSidebarVisible - Initial sidebar visibility state
* @param {boolean} options.isMobileView - Whether the current view is mobile
* @returns {Object} Pure tab management utilities and state
*/
const useCourseTabsState = (options = {}) => {
const {
tabMap: customTabMap,
initialSidebarVisible,
isMobileView = false
} = options;
// Tab management state
const [activeTab, setActiveTab] = useState('overview');
const [sidebarVisible, setSidebarVisible] = useState(
initialSidebarVisible !== undefined ? initialSidebarVisible : !isMobileView
);
// Track if we've initialized yet
const initialized = useRef(false);
// Get tab map based on view mode
const tabMap = useMemo(() => {
const baseTabMap = customTabMap || ['overview', 'content', 'qa'];
if (isMobileView) {
const mobileTabMap = [...baseTabMap];
// Insert lessons tab before qa in mobile view
if (!mobileTabMap.includes('lessons')) {
mobileTabMap.splice(2, 0, 'lessons');
}
return mobileTabMap;
}
return baseTabMap;
}, [isMobileView, customTabMap]);
// Auto-update sidebar visibility based on mobile/desktop
useEffect(() => {
if (initialized.current) {
// Only auto-update sidebar visibility if we're initialized
// and the view mode changes
setSidebarVisible(!isMobileView);
} else {
initialized.current = true;
}
}, [isMobileView]);
// Get active tab index
const getActiveTabIndex = useCallback(() => {
return tabMap.indexOf(activeTab);
}, [activeTab, tabMap]);
// Pure toggle between tabs with no side effects
const toggleTab = useCallback((indexOrName) => {
const tabName = typeof indexOrName === 'number'
? tabMap[indexOrName]
: indexOrName;
setActiveTab(tabName);
// Only show/hide sidebar on mobile - desktop keeps sidebar visible
if (isMobileView) {
setSidebarVisible(tabName === 'lessons');
}
}, [tabMap, isMobileView]);
// Toggle sidebar visibility
const toggleSidebar = useCallback(() => {
setSidebarVisible(prev => !prev);
}, []);
// Generate tab items for MenuTab component
const getTabItems = useCallback(() => {
const items = [
{
label: 'Overview',
icon: 'pi pi-home',
},
{
label: 'Content',
icon: 'pi pi-book',
}
];
// Add lessons tab only on mobile
if (isMobileView) {
items.push({
label: 'Lessons',
icon: 'pi pi-list',
});
}
items.push({
label: 'Comments',
icon: 'pi pi-comments',
});
return items;
}, [isMobileView]);
// Setup keyboard navigation for tabs
useEffect(() => {
const handleKeyDown = (e) => {
if (e.key === 'ArrowRight') {
const currentIndex = getActiveTabIndex();
const nextIndex = (currentIndex + 1) % tabMap.length;
toggleTab(nextIndex);
} else if (e.key === 'ArrowLeft') {
const currentIndex = getActiveTabIndex();
const prevIndex = (currentIndex - 1 + tabMap.length) % tabMap.length;
toggleTab(prevIndex);
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [getActiveTabIndex, tabMap, toggleTab]);
return {
activeTab,
setActiveTab,
sidebarVisible,
setSidebarVisible,
toggleTab,
toggleSidebar,
getActiveTabIndex,
getTabItems,
tabMap
};
};
export default useCourseTabsState;