mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-06-05 00:32:03 +00:00
extract course logic into reusable hooks
This commit is contained in:
parent
f526913f30
commit
045418397c
@ -11,7 +11,8 @@ const appConfig = {
|
|||||||
],
|
],
|
||||||
authorPubkeys: [
|
authorPubkeys: [
|
||||||
'f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741',
|
'f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741',
|
||||||
'c67cd3e1a83daa56cff16f635db2fdb9ed9619300298d4701a58e68e84098345'
|
'c67cd3e1a83daa56cff16f635db2fdb9ed9619300298d4701a58e68e84098345',
|
||||||
|
'6260f29fa75c91aaa292f082e5e87b438d2ab4fdf96af398567b01802ee2fcd4'
|
||||||
],
|
],
|
||||||
customLightningAddresses: [
|
customLightningAddresses: [
|
||||||
{
|
{
|
||||||
|
9
src/hooks/courses/index.js
Normal file
9
src/hooks/courses/index.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import useCourseDecryption from '../encryption/useCourseDecryption';
|
||||||
|
import useCourseTabs from './useCourseTabs';
|
||||||
|
import useCoursePayment from './useCoursePayment';
|
||||||
|
|
||||||
|
export {
|
||||||
|
useCourseDecryption,
|
||||||
|
useCourseTabs,
|
||||||
|
useCoursePayment
|
||||||
|
};
|
67
src/hooks/courses/useCoursePayment.js
Normal file
67
src/hooks/courses/useCoursePayment.js
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import { useToast } from '../useToast';
|
||||||
|
import { useSession } from 'next-auth/react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to handle course payment processing and authorization
|
||||||
|
* @param {Object} course - The course object
|
||||||
|
* @returns {Object} Payment handling utilities and authorization state
|
||||||
|
*/
|
||||||
|
const useCoursePayment = (course) => {
|
||||||
|
const { data: session, update } = useSession();
|
||||||
|
const { showToast } = useToast();
|
||||||
|
|
||||||
|
// Determine if course requires payment
|
||||||
|
const isPaidCourse = useMemo(() => {
|
||||||
|
return course?.price && course.price > 0;
|
||||||
|
}, [course]);
|
||||||
|
|
||||||
|
// Check if user is authorized to access the course
|
||||||
|
const isAuthorized = useMemo(() => {
|
||||||
|
if (!session?.user || !course) return !isPaidCourse; // Free courses are always authorized
|
||||||
|
|
||||||
|
return (
|
||||||
|
// User is subscribed
|
||||||
|
session.user.role?.subscribed ||
|
||||||
|
// User is the creator of the course
|
||||||
|
session.user.pubkey === course.pubkey ||
|
||||||
|
// Course is free
|
||||||
|
!isPaidCourse ||
|
||||||
|
// User has purchased this specific course
|
||||||
|
session.user.purchased?.some(purchase => purchase.courseId === course.d)
|
||||||
|
);
|
||||||
|
}, [session, course, isPaidCourse]);
|
||||||
|
|
||||||
|
// Handler for successful payment
|
||||||
|
const handlePaymentSuccess = useCallback(async (response) => {
|
||||||
|
if (response && response?.preimage) {
|
||||||
|
// Update session to reflect purchase
|
||||||
|
const updated = await update();
|
||||||
|
showToast('success', 'Payment Success', 'You have successfully purchased this course');
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
showToast('error', 'Error', 'Failed to purchase course. Please try again.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, [update, showToast]);
|
||||||
|
|
||||||
|
// Handler for payment errors
|
||||||
|
const handlePaymentError = useCallback((error) => {
|
||||||
|
showToast(
|
||||||
|
'error',
|
||||||
|
'Payment Error',
|
||||||
|
`Failed to purchase course. Please try again. Error: ${error}`
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}, [showToast]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isPaidCourse,
|
||||||
|
isAuthorized,
|
||||||
|
handlePaymentSuccess,
|
||||||
|
handlePaymentError,
|
||||||
|
session
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useCoursePayment;
|
141
src/hooks/courses/useCourseTabs.js
Normal file
141
src/hooks/courses/useCourseTabs.js
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import { useState, useEffect, useMemo, useCallback } from 'react';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import useWindowWidth from '../useWindowWidth';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to manage course tabs, navigation, 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
|
||||||
|
* @returns {Object} Tab management utilities and state
|
||||||
|
*/
|
||||||
|
const useCourseTabs = (options = {}) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const windowWidth = useWindowWidth();
|
||||||
|
const isMobileView = windowWidth <= 968;
|
||||||
|
|
||||||
|
// 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]);
|
||||||
|
|
||||||
|
// Update tabs and sidebar based on router query
|
||||||
|
useEffect(() => {
|
||||||
|
if (router.isReady) {
|
||||||
|
const { active } = router.query;
|
||||||
|
if (active !== undefined) {
|
||||||
|
// If we have an active lesson, switch to content tab
|
||||||
|
setActiveTab('content');
|
||||||
|
} else {
|
||||||
|
// Default to overview tab when no active parameter
|
||||||
|
setActiveTab('overview');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-open sidebar on desktop, close on mobile
|
||||||
|
setSidebarVisible(!isMobileView);
|
||||||
|
}
|
||||||
|
}, [router.isReady, router.query, isMobileView]);
|
||||||
|
|
||||||
|
// Get active tab index
|
||||||
|
const getActiveTabIndex = useCallback(() => {
|
||||||
|
return tabMap.indexOf(activeTab);
|
||||||
|
}, [activeTab, tabMap]);
|
||||||
|
|
||||||
|
// Toggle between tabs
|
||||||
|
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,
|
||||||
|
isMobileView,
|
||||||
|
toggleTab,
|
||||||
|
toggleSidebar,
|
||||||
|
getActiveTabIndex,
|
||||||
|
getTabItems,
|
||||||
|
tabMap
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useCourseTabs;
|
Loading…
x
Reference in New Issue
Block a user