diff --git a/frontend/src/components/ModuleCard.tsx b/frontend/src/components/ModuleCard.tsx
index 7a11201fba..a7fdaf495e 100644
--- a/frontend/src/components/ModuleCard.tsx
+++ b/frontend/src/components/ModuleCard.tsx
@@ -2,29 +2,221 @@ import { capitalize } from 'lodash'
import Image from 'next/image'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
-import type React from 'react'
-import { useState } from 'react'
-import { FaChevronDown, FaChevronUp, FaTurnUp, FaCalendar, FaHourglassHalf } from 'react-icons/fa6'
+import React, { useState } from 'react'
+import { FaChevronDown, FaChevronUp, FaTurnUp, FaCalendar, FaHourglassHalf, FaGripVertical } from 'react-icons/fa6'
+import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'
+import { SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy, arrayMove } from '@dnd-kit/sortable'
+import { useSortable } from '@dnd-kit/sortable'
+import { CSS } from '@dnd-kit/utilities'
+import { useMutation } from '@apollo/client'
import type { Module } from 'types/mentorship'
import { formatDate } from 'utils/dateFormatter'
import { TextInfoItem } from 'components/InfoItem'
import SingleModuleCard from 'components/SingleModuleCard'
import { TruncatedText } from 'components/TruncatedText'
+import { UPDATE_MODULE_POSITIONS } from 'server/queries/moduleQueries'
interface ModuleCardProps {
modules: Module[]
accessLevel?: string
admins?: { login: string }[]
+ programKey?: string
+ enableReordering?: boolean
}
-const ModuleCard = ({ modules, accessLevel, admins }: ModuleCardProps) => {
+interface SortableModuleItemProps {
+ module: Module
+ programKey?: string
+}
+
+const SortableModuleItem = ({ module, programKey }: SortableModuleItemProps) => {
+ const {
+ attributes,
+ listeners,
+ setNodeRef,
+ transform,
+ transition,
+ isDragging,
+ } = useSortable({ id: module.id })
+
+ const style = {
+ transform: CSS.Transform.toString(transform),
+ transition,
+ opacity: isDragging ? 0.5 : 1,
+ }
+
+ const pathname = usePathname()
+
+ const mentors = module.mentors || []
+ const mentees = module.mentees || []
+
+ const mentorsWithAvatars = mentors.filter((m) => m?.avatarUrl)
+ const menteesWithAvatars = mentees.filter((m) => m?.avatarUrl)
+
+ const moduleKey = module.key || module.id
+
+ const getMenteeUrl = (login: string) => {
+ if (pathname?.startsWith('/my/mentorship')) {
+ return `/my/mentorship/programs/${programKey}/modules/${moduleKey}/mentees/${login}`
+ }
+ return `/members/${login}`
+ }
+
+ const getAvatarUrlWithSize = (avatarUrl: string): string => {
+ try {
+ const url = new URL(avatarUrl)
+ url.searchParams.set('s', '60')
+ return url.toString()
+ } catch {
+ const separator = avatarUrl.includes('?') ? '&' : '?'
+ return `${avatarUrl}${separator}s=60`
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ {(mentorsWithAvatars.length > 0 || menteesWithAvatars.length > 0) && (
+
+ {mentorsWithAvatars.length > 0 && (
+
+
+ Mentors
+
+
+ {mentorsWithAvatars.slice(0, 4).map((contributor) => (
+
+
+
+ ))}
+ {mentorsWithAvatars.length > 4 && (
+
+ +{mentorsWithAvatars.length - 4}
+
+ )}
+
+
+ )}
+ {menteesWithAvatars.length > 0 && (
+
0 ? 'border-l-1 border-gray-100 pl-4 dark:border-gray-700' : ''}`}
+ >
+
+ Mentees
+
+
+ {menteesWithAvatars.slice(0, 4).map((contributor) => (
+
+
+
+ ))}
+ {menteesWithAvatars.length > 4 && (
+
+ +{menteesWithAvatars.length - 4}
+
+ )}
+
+
+ )}
+
+ )}
+
+
+ )
+}
+
+const ModuleCard = ({ modules, accessLevel, admins, programKey, enableReordering = false }: ModuleCardProps) => {
const [showAllModule, setShowAllModule] = useState(false)
+ const [modulesList, setModulesList] = useState(modules)
+
+ const [updateModulePositions] = useMutation(UPDATE_MODULE_POSITIONS)
+
+ const sensors = useSensors(
+ useSensor(PointerSensor),
+ useSensor(KeyboardSensor, {
+ coordinateGetter: sortableKeyboardCoordinates,
+ })
+ )
+
+ React.useEffect(() => {
+ setModulesList(modules)
+ }, [modules])
+
+ const handleDragEnd = async (event: any) => {
+ const { active, over } = event
+
+ if (active.id !== over.id) {
+ const oldIndex = modulesList.findIndex((module) => module.id === active.id)
+ const newIndex = modulesList.findIndex((module) => module.id === over.id)
+
+ const newModulesList = arrayMove(modulesList, oldIndex, newIndex)
+ setModulesList(newModulesList)
+
+ // Prepare module positions for mutation
+ const modulePositions = newModulesList.map((module, index) => ({
+ moduleId: module.id,
+ position: index,
+ }))
+
+ try {
+ await updateModulePositions({
+ variables: {
+ programKey,
+ modulePositions,
+ },
+ })
+ } catch (error) {
+ console.error('Failed to update module positions:', error)
+ // Revert to original order on error
+ setModulesList(modules)
+ }
+ }
+ }
if (modules.length === 1) {
return
}
- const displayedModule = showAllModule ? modules : modules.slice(0, 4)
+ const displayedModule = showAllModule ? modulesList : modulesList.slice(0, 4)
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
@@ -33,6 +225,47 @@ const ModuleCard = ({ modules, accessLevel, admins }: ModuleCardProps) => {
}
}
+ if (enableReordering) {
+ return (
+
+ module.id)}
+ strategy={verticalListSortingStrategy}
+ >
+
+ {displayedModule.map((module) => (
+
+ ))}
+
+
+ {modulesList.length > 4 && (
+
+
+
+ )}
+
+ )
+ }
+
return (
diff --git a/frontend/src/server/queries/moduleQueries.ts b/frontend/src/server/queries/moduleQueries.ts
index a21f879a6b..8a9539b84a 100644
--- a/frontend/src/server/queries/moduleQueries.ts
+++ b/frontend/src/server/queries/moduleQueries.ts
@@ -141,3 +141,18 @@ export const GET_MODULE_ISSUES = gql`
}
}
`
+
+export const UPDATE_MODULE_POSITIONS = gql`
+ mutation UpdateModulePositions($programKey: String!, $modulePositions: [ModulePositionInput!]!) {
+ updateModulePositions(programKey: $programKey, modulePositions: $modulePositions) {
+ success
+ message
+ updatedModules {
+ id
+ key
+ name
+ position
+ }
+ }
+ }
+`
diff --git a/frontend/src/types/mentorship.ts b/frontend/src/types/mentorship.ts
index 71fbbb18e7..f8cc7ee914 100644
--- a/frontend/src/types/mentorship.ts
+++ b/frontend/src/types/mentorship.ts
@@ -47,6 +47,7 @@ export type Module = {
mentees?: Contributor[]
mentors: Contributor[]
name: string
+ position?: number
startedAt: string | number
status?: ProgramStatusEnum
tags?: string[] | null