mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: add the ability for the user to apply for contributions
This commit is contained in:
parent
4a3176d702
commit
9db6b605c5
1 changed files with 165 additions and 29 deletions
|
|
@ -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,6 +482,7 @@ const Actions = ({ courseuuid, orgslug, course }: CourseActionsProps) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
<button
|
<button
|
||||||
onClick={handleCourseAction}
|
onClick={handleCourseAction}
|
||||||
disabled={isActionLoading}
|
disabled={isActionLoading}
|
||||||
|
|
@ -377,6 +511,8 @@ const Actions = ({ courseuuid, orgslug, course }: CourseActionsProps) => {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
{renderContributorButton()}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue