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 useCourseData from './useCourseData';
import useLessons from './useLessons';
import useCourseNavigation from './useCourseNavigation';
import useCourseTabsState from './useCourseTabsState';
export {
useCourseDecryption,
useCourseTabs,
useCoursePayment,
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
@ -8,19 +9,20 @@ import { useState, useEffect, useMemo } from 'react';
*/
const useCourseNavigation = (router, isMobileView) => {
const [activeIndex, setActiveIndex] = useState(0);
const [sidebarVisible, setSidebarVisible] = useState(false);
const [activeTab, setActiveTab] = useState('overview'); // Default to overview tab
// Memoized function to get the tab map based on view mode
const tabMap = useMemo(() => {
const baseTabMap = ['overview', 'content', 'qa'];
if (isMobileView) {
const mobileTabMap = [...baseTabMap];
mobileTabMap.splice(2, 0, 'lessons');
return mobileTabMap;
}
return baseTabMap;
}, [isMobileView]);
// Use the base hook for core tab state functionality
const {
activeTab,
setActiveTab,
sidebarVisible,
setSidebarVisible,
tabMap,
getActiveTabIndex,
getTabItems,
toggleSidebar: baseToggleSidebar
} = useCourseTabsState({
isMobileView
});
// Initialize navigation state based on router
useEffect(() => {
@ -39,10 +41,10 @@ const useCourseNavigation = (router, isMobileView) => {
// Auto-open sidebar on desktop, close on mobile
setSidebarVisible(!isMobileView);
}
}, [router.isReady, router.query, isMobileView]);
}, [router.isReady, router.query, isMobileView, setActiveTab, setSidebarVisible]);
// Function to handle lesson selection
const handleLessonSelect = (index) => {
const handleLessonSelect = useCallback((index) => {
setActiveIndex(index);
// Update URL without causing a page reload (for bookmarking purposes)
@ -54,10 +56,10 @@ const useCourseNavigation = (router, isMobileView) => {
setActiveTab('content');
setSidebarVisible(false);
}
};
}, [router.query.slug, isMobileView, setActiveTab, setSidebarVisible]);
// Function to toggle tab
const toggleTab = (index) => {
// Function to toggle tab with lesson state integration
const toggleTab = useCallback((index) => {
const tabName = tabMap[index];
setActiveTab(tabName);
@ -65,66 +67,7 @@ const useCourseNavigation = (router, isMobileView) => {
if (isMobileView) {
setSidebarVisible(tabName === 'lessons');
}
};
// 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]);
}, [tabMap, isMobileView, setActiveTab, setSidebarVisible]);
return {
activeIndex,
@ -135,7 +78,7 @@ const useCourseNavigation = (router, isMobileView) => {
setSidebarVisible,
handleLessonSelect,
toggleTab,
toggleSidebar,
toggleSidebar: baseToggleSidebar,
getActiveTabIndex,
getTabItems,
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 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
* @param {Object} options - Configuration options
* @param {Array} options.tabMap - Optional custom tab map to use
@ -13,49 +15,42 @@ const useCourseTabs = (options = {}) => {
const router = useRouter();
const windowWidth = useWindowWidth();
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
const tabMap = useMemo(() => {
const baseTabMap = options.tabMap || ['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, options.tabMap]);
// Use the base hook for core tab state functionality
const {
activeTab,
setActiveTab,
sidebarVisible,
setSidebarVisible,
tabMap,
getActiveTabIndex,
getTabItems,
toggleSidebar
} = useCourseTabsState({
tabMap: options.tabMap,
initialSidebarVisible: options.initialSidebarVisible,
isMobileView
});
// Update tabs and sidebar based on router query
useEffect(() => {
if (router.isReady) {
const { active } = router.query;
if (active !== undefined) {
const { active, tab } = router.query;
// 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
setActiveTab('content');
} else {
// Default to overview tab when no active parameter
// Default to overview tab when no parameters
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
const getActiveTabIndex = useCallback(() => {
return tabMap.indexOf(activeTab);
}, [activeTab, tabMap]);
// Toggle between tabs
// Toggle between tabs with router integration
const toggleTab = useCallback((indexOrName) => {
const tabName = typeof indexOrName === 'number'
? tabMap[indexOrName]
@ -67,61 +62,18 @@ const useCourseTabs = (options = {}) => {
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);
}
// Sync URL with tab change using shallow routing
const newQuery = {
...router.query,
tab: tabName === 'overview' ? undefined : tabName
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [getActiveTabIndex, tabMap, toggleTab]);
router.push(
{ pathname: router.pathname, query: newQuery },
undefined,
{ shallow: true }
);
}, [tabMap, isMobileView, router, setActiveTab, setSidebarVisible]);
return {
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;