diff --git a/src/hooks/courses/index.js b/src/hooks/courses/index.js index f6438e0..82bc2e2 100644 --- a/src/hooks/courses/index.js +++ b/src/hooks/courses/index.js @@ -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 }; \ No newline at end of file diff --git a/src/hooks/courses/useCourseNavigation.js b/src/hooks/courses/useCourseNavigation.js index 7484c74..571718c 100644 --- a/src/hooks/courses/useCourseNavigation.js +++ b/src/hooks/courses/useCourseNavigation.js @@ -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 diff --git a/src/hooks/courses/useCourseTabs.js b/src/hooks/courses/useCourseTabs.js index dffcda8..f0ab071 100644 --- a/src/hooks/courses/useCourseTabs.js +++ b/src/hooks/courses/useCourseTabs.js @@ -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, diff --git a/src/hooks/courses/useCourseTabsState.js b/src/hooks/courses/useCourseTabsState.js new file mode 100644 index 0000000..bb3bc06 --- /dev/null +++ b/src/hooks/courses/useCourseTabsState.js @@ -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; \ No newline at end of file