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 { getUriWithOrg, getUriWithoutOrg } from '@services/config/config'
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 CoursePaidOptions from './CoursePaidOptions'
import { checkPaidAccess } from '@services/payments/payments'
import { applyForContributor, getCourseContributors } from '@services/courses/courses'
import toast from 'react-hot-toast'
interface Author {
user: {
@ -44,6 +46,7 @@ interface Course {
activity_type: string
}>
}>
open_to_contributors?: boolean
}
interface CourseActionsProps {
@ -186,8 +189,10 @@ const Actions = ({ courseuuid, orgslug, course }: CourseActionsProps) => {
const [linkedProducts, setLinkedProducts] = useState<any[]>([])
const [isLoading, setIsLoading] = useState(true)
const [isActionLoading, setIsActionLoading] = useState(false)
const [isContributeLoading, setIsContributeLoading] = useState(false)
const [isModalOpen, setIsModalOpen] = useState(false)
const [hasAccess, setHasAccess] = useState<boolean | null>(null)
const [contributorStatus, setContributorStatus] = useState<'NONE' | 'PENDING' | 'ACTIVE' | 'INACTIVE'>('NONE')
const isStarted = course.trail?.runs?.some(
(run) => run.status === 'STATUS_IN_PROGRESS' && run.course_id === course.id
@ -212,6 +217,39 @@ const Actions = ({ courseuuid, orgslug, course }: CourseActionsProps) => {
fetchLinkedProducts()
}, [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(() => {
const checkAccess = async () => {
if (!session.data?.user) return
@ -225,6 +263,7 @@ const Actions = ({ courseuuid, orgslug, course }: CourseActionsProps) => {
} catch (error) {
console.error('Failed to check course access')
toast.error('Failed to check course access. Please try again later.')
setHasAccess(false)
}
}
@ -241,14 +280,20 @@ const Actions = ({ courseuuid, orgslug, course }: CourseActionsProps) => {
}
setIsActionLoading(true)
const loadingToast = toast.loading(
isStarted ? 'Leaving course...' : 'Starting course...'
)
try {
if (isStarted) {
await removeCourse('course_' + courseuuid, orgslug, session.data?.tokens?.access_token)
await revalidateTags(['courses'], orgslug)
toast.success('Successfully left the course', { id: loadingToast })
router.refresh()
} else {
await startCourse('course_' + courseuuid, orgslug, session.data?.tokens?.access_token)
await revalidateTags(['courses'], orgslug)
toast.success('Successfully started the course', { id: loadingToast })
// Get the first activity from the first chapter
const firstChapter = course.chapters?.[0]
@ -266,15 +311,101 @@ const Actions = ({ courseuuid, orgslug, course }: CourseActionsProps) => {
}
} catch (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 {
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) {
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) {
return (
<div className="space-y-4">
@ -312,6 +443,7 @@ const Actions = ({ courseuuid, orgslug, course }: CourseActionsProps) => {
</>
)}
</button>
{renderContributorButton()}
</>
) : (
<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" />
Purchase Course
</button>
{renderContributorButton()}
</>
)}
</div>
@ -349,6 +482,7 @@ const Actions = ({ courseuuid, orgslug, course }: CourseActionsProps) => {
}
return (
<div className="space-y-4">
<button
onClick={handleCourseAction}
disabled={isActionLoading}
@ -377,6 +511,8 @@ const Actions = ({ courseuuid, orgslug, course }: CourseActionsProps) => {
</>
)}
</button>
{renderContributorButton()}
</div>
)
}