mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-06-05 00:32:03 +00:00
deduplicate course tab logic, add pure useCourseTabsState, and sync tab state with URL
This commit is contained in:
parent
5a79523ba0
commit
ccda05df96
@ -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
|
||||||
};
|
};
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
140
src/hooks/courses/useCourseTabsState.js
Normal file
140
src/hooks/courses/useCourseTabsState.js
Normal 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;
|
Loading…
x
Reference in New Issue
Block a user