feat: add the ability for the user to apply for contributions

This commit is contained in:
swve 2025-03-22 14:19:39 +01:00
parent 4a3176d702
commit 9db6b605c5

View file

@ -8,10 +8,12 @@ import { useLHSession } from '@components/Contexts/LHSessionContext'
import { useMediaQuery } from 'usehooks-ts' import { useMediaQuery } from 'usehooks-ts'
import { getUriWithOrg, getUriWithoutOrg } from '@services/config/config' import { getUriWithOrg, getUriWithoutOrg } from '@services/config/config'
import { getProductsByCourse } from '@services/payments/products' import { getProductsByCourse } from '@services/payments/products'
import { LogIn, LogOut, ShoppingCart, AlertCircle } from 'lucide-react' import { LogIn, LogOut, ShoppingCart, AlertCircle, UserPen, ClockIcon } from 'lucide-react'
import Modal from '@components/Objects/StyledElements/Modal/Modal' import Modal from '@components/Objects/StyledElements/Modal/Modal'
import CoursePaidOptions from './CoursePaidOptions' import CoursePaidOptions from './CoursePaidOptions'
import { checkPaidAccess } from '@services/payments/payments' import { checkPaidAccess } from '@services/payments/payments'
import { applyForContributor, getCourseContributors } from '@services/courses/courses'
import toast from 'react-hot-toast'
interface Author { interface Author {
user: { user: {
@ -44,6 +46,7 @@ interface Course {
activity_type: string activity_type: string
}> }>
}> }>
open_to_contributors?: boolean
} }
interface CourseActionsProps { interface CourseActionsProps {
@ -186,8 +189,10 @@ const Actions = ({ courseuuid, orgslug, course }: CourseActionsProps) => {
const [linkedProducts, setLinkedProducts] = useState<any[]>([]) const [linkedProducts, setLinkedProducts] = useState<any[]>([])
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const [isActionLoading, setIsActionLoading] = useState(false) const [isActionLoading, setIsActionLoading] = useState(false)
const [isContributeLoading, setIsContributeLoading] = useState(false)
const [isModalOpen, setIsModalOpen] = useState(false) const [isModalOpen, setIsModalOpen] = useState(false)
const [hasAccess, setHasAccess] = useState<boolean | null>(null) const [hasAccess, setHasAccess] = useState<boolean | null>(null)
const [contributorStatus, setContributorStatus] = useState<'NONE' | 'PENDING' | 'ACTIVE' | 'INACTIVE'>('NONE')
const isStarted = course.trail?.runs?.some( const isStarted = course.trail?.runs?.some(
(run) => run.status === 'STATUS_IN_PROGRESS' && run.course_id === course.id (run) => run.status === 'STATUS_IN_PROGRESS' && run.course_id === course.id
@ -212,6 +217,39 @@ const Actions = ({ courseuuid, orgslug, course }: CourseActionsProps) => {
fetchLinkedProducts() fetchLinkedProducts()
}, [course.id, course.org_id, session.data?.tokens?.access_token]) }, [course.id, course.org_id, session.data?.tokens?.access_token])
// Check if the current user is already a contributor
useEffect(() => {
const checkContributorStatus = async () => {
if (!session.data?.user) return
try {
const response = await getCourseContributors(
'course_' + courseuuid,
session.data?.tokens?.access_token
)
if (response && response.data) {
const currentUser = response.data.find(
(contributor: any) => contributor.user_id === session.data.user.id
)
if (currentUser) {
setContributorStatus(currentUser.authorship_status as 'PENDING' | 'ACTIVE' | 'INACTIVE')
} else {
setContributorStatus('NONE')
}
}
} catch (error) {
console.error('Failed to check contributor status:', error)
toast.error('Failed to check contributor status. Please try again later.')
}
}
if (session.data?.user) {
checkContributorStatus()
}
}, [courseuuid, session.data?.tokens?.access_token, session.data?.user])
useEffect(() => { useEffect(() => {
const checkAccess = async () => { const checkAccess = async () => {
if (!session.data?.user) return if (!session.data?.user) return
@ -225,6 +263,7 @@ const Actions = ({ courseuuid, orgslug, course }: CourseActionsProps) => {
} catch (error) { } catch (error) {
console.error('Failed to check course access') console.error('Failed to check course access')
toast.error('Failed to check course access. Please try again later.')
setHasAccess(false) setHasAccess(false)
} }
} }
@ -241,14 +280,20 @@ const Actions = ({ courseuuid, orgslug, course }: CourseActionsProps) => {
} }
setIsActionLoading(true) setIsActionLoading(true)
const loadingToast = toast.loading(
isStarted ? 'Leaving course...' : 'Starting course...'
)
try { try {
if (isStarted) { if (isStarted) {
await removeCourse('course_' + courseuuid, orgslug, session.data?.tokens?.access_token) await removeCourse('course_' + courseuuid, orgslug, session.data?.tokens?.access_token)
await revalidateTags(['courses'], orgslug) await revalidateTags(['courses'], orgslug)
toast.success('Successfully left the course', { id: loadingToast })
router.refresh() router.refresh()
} else { } else {
await startCourse('course_' + courseuuid, orgslug, session.data?.tokens?.access_token) await startCourse('course_' + courseuuid, orgslug, session.data?.tokens?.access_token)
await revalidateTags(['courses'], orgslug) await revalidateTags(['courses'], orgslug)
toast.success('Successfully started the course', { id: loadingToast })
// Get the first activity from the first chapter // Get the first activity from the first chapter
const firstChapter = course.chapters?.[0] const firstChapter = course.chapters?.[0]
@ -266,15 +311,101 @@ const Actions = ({ courseuuid, orgslug, course }: CourseActionsProps) => {
} }
} catch (error) { } catch (error) {
console.error('Failed to perform course action:', error) console.error('Failed to perform course action:', error)
toast.error(
isStarted
? 'Failed to leave the course. Please try again later.'
: 'Failed to start the course. Please try again later.',
{ id: loadingToast }
)
} finally { } finally {
setIsActionLoading(false) setIsActionLoading(false)
} }
} }
const handleApplyToContribute = async () => {
if (!session.data?.user) {
router.push(getUriWithoutOrg(`/signup?orgslug=${orgslug}`))
return
}
setIsContributeLoading(true)
const loadingToast = toast.loading('Submitting contributor application...')
try {
const data = {
message: "I would like to contribute to this course."
}
await applyForContributor('course_' + courseuuid, data, session.data?.tokens?.access_token)
setContributorStatus('PENDING')
await revalidateTags(['courses'], orgslug)
toast.success('Your application to contribute has been submitted successfully', { id: loadingToast })
} catch (error) {
console.error('Failed to apply as contributor:', error)
toast.error('Failed to submit your application. Please try again later.', { id: loadingToast })
} finally {
setIsContributeLoading(false)
}
}
if (isLoading) { if (isLoading) {
return <div className="animate-pulse h-20 bg-gray-100 rounded-lg nice-shadow" /> return <div className="animate-pulse h-20 bg-gray-100 rounded-lg nice-shadow" />
} }
const renderContributorButton = () => {
// Don't render anything if the course is not open to contributors or if the user status is INACTIVE
if (contributorStatus === 'INACTIVE' || course.open_to_contributors !== true) {
return null;
}
if (!session.data?.user) {
return (
<button
onClick={() => router.push(getUriWithoutOrg(`/signup?orgslug=${orgslug}`))}
className="w-full bg-white text-neutral-700 border border-neutral-200 py-3 rounded-lg nice-shadow font-semibold hover:bg-neutral-50 transition-colors flex items-center justify-center gap-2 mt-3 cursor-pointer"
>
<UserPen className="w-5 h-5" />
Authenticate to contribute
</button>
);
}
if (contributorStatus === 'ACTIVE') {
return (
<div className="w-full bg-green-50 text-green-700 border border-green-200 py-3 rounded-lg nice-shadow font-semibold flex items-center justify-center gap-2 mt-3">
<UserPen className="w-5 h-5" />
You are a contributor
</div>
);
}
if (contributorStatus === 'PENDING') {
return (
<div className="w-full bg-amber-50 text-amber-700 border border-amber-200 py-3 rounded-lg nice-shadow font-semibold flex items-center justify-center gap-2 mt-3">
<ClockIcon className="w-5 h-5" />
Contributor application pending
</div>
);
}
return (
<button
onClick={handleApplyToContribute}
disabled={isContributeLoading}
className="w-full bg-white text-neutral-700 py-3 rounded-lg nice-shadow font-semibold hover:bg-neutral-50 transition-colors flex items-center justify-center gap-2 mt-3 cursor-pointer disabled:cursor-not-allowed"
>
{isContributeLoading ? (
<div className="w-5 h-5 border-2 border-neutral-700 border-t-transparent rounded-full animate-spin" />
) : (
<>
<UserPen className="w-5 h-5" />
Apply to contribute
</>
)}
</button>
);
};
if (linkedProducts.length > 0) { if (linkedProducts.length > 0) {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
@ -312,6 +443,7 @@ const Actions = ({ courseuuid, orgslug, course }: CourseActionsProps) => {
</> </>
)} )}
</button> </button>
{renderContributorButton()}
</> </>
) : ( ) : (
<div className="p-4 bg-amber-50 border border-amber-200 rounded-lg nice-shadow"> <div className="p-4 bg-amber-50 border border-amber-200 rounded-lg nice-shadow">
@ -342,6 +474,7 @@ const Actions = ({ courseuuid, orgslug, course }: CourseActionsProps) => {
<ShoppingCart className="w-5 h-5" /> <ShoppingCart className="w-5 h-5" />
Purchase Course Purchase Course
</button> </button>
{renderContributorButton()}
</> </>
)} )}
</div> </div>
@ -349,34 +482,37 @@ const Actions = ({ courseuuid, orgslug, course }: CourseActionsProps) => {
} }
return ( return (
<button <div className="space-y-4">
onClick={handleCourseAction} <button
disabled={isActionLoading} onClick={handleCourseAction}
className={`w-full py-3 rounded-lg nice-shadow font-semibold transition-colors flex items-center justify-center gap-2 cursor-pointer ${ disabled={isActionLoading}
isStarted className={`w-full py-3 rounded-lg nice-shadow font-semibold transition-colors flex items-center justify-center gap-2 cursor-pointer ${
? 'bg-red-500 text-white hover:bg-red-600 disabled:bg-red-400' isStarted
: 'bg-neutral-900 text-white hover:bg-neutral-800 disabled:bg-neutral-700' ? 'bg-red-500 text-white hover:bg-red-600 disabled:bg-red-400'
}`} : 'bg-neutral-900 text-white hover:bg-neutral-800 disabled:bg-neutral-700'
> }`}
{isActionLoading ? ( >
<div className="w-6 h-6 border-2 border-white border-t-transparent rounded-full animate-spin" /> {isActionLoading ? (
) : !session.data?.user ? ( <div className="w-6 h-6 border-2 border-white border-t-transparent rounded-full animate-spin" />
<> ) : !session.data?.user ? (
<LogIn className="w-5 h-5" /> <>
Authenticate to start course <LogIn className="w-5 h-5" />
</> Authenticate to start course
) : isStarted ? ( </>
<> ) : isStarted ? (
<LogOut className="w-5 h-5" /> <>
Leave Course <LogOut className="w-5 h-5" />
</> Leave Course
) : ( </>
<> ) : (
<LogIn className="w-5 h-5" /> <>
Start Course <LogIn className="w-5 h-5" />
</> Start Course
)} </>
</button> )}
</button>
{renderContributorButton()}
</div>
) )
} }