mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: Add User Courses in Profile
This commit is contained in:
parent
31c27bb70e
commit
b1aec48947
6 changed files with 233 additions and 8 deletions
|
|
@ -15,9 +15,14 @@ import {
|
|||
Users,
|
||||
Calendar,
|
||||
Lightbulb,
|
||||
X
|
||||
X,
|
||||
ExternalLink
|
||||
} from 'lucide-react'
|
||||
import { getUserAvatarMediaDirectory } from '@services/media/media'
|
||||
import { getCoursesByUser } from '@services/users/users'
|
||||
import { useLHSession } from '@components/Contexts/LHSessionContext'
|
||||
import { Button } from "@components/ui/button"
|
||||
import CourseThumbnailLanding from '@components/Objects/Thumbnails/CourseThumbnailLanding'
|
||||
|
||||
interface UserProfileClientProps {
|
||||
userData: any;
|
||||
|
|
@ -67,7 +72,31 @@ const ImageModal: React.FC<{
|
|||
};
|
||||
|
||||
function UserProfileClient({ userData, profile }: UserProfileClientProps) {
|
||||
const session = useLHSession() as any
|
||||
const access_token = session?.data?.tokens?.access_token
|
||||
const [selectedImage, setSelectedImage] = React.useState<{ url: string; caption?: string } | null>(null);
|
||||
const [userCourses, setUserCourses] = React.useState<any[]>([]);
|
||||
const [isLoadingCourses, setIsLoadingCourses] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
const fetchUserCourses = async () => {
|
||||
if (userData.id && access_token) {
|
||||
try {
|
||||
setIsLoadingCourses(true);
|
||||
const coursesData = await getCoursesByUser(userData.id, access_token);
|
||||
if (coursesData.data) {
|
||||
setUserCourses(coursesData.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching user courses:', error);
|
||||
} finally {
|
||||
setIsLoadingCourses(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fetchUserCourses();
|
||||
}, [userData.id, access_token]);
|
||||
|
||||
const IconComponent = ({ iconName }: { iconName: string }) => {
|
||||
const IconElement = ICON_MAP[iconName as keyof typeof ICON_MAP]
|
||||
|
|
@ -273,6 +302,31 @@ function UserProfileClient({ userData, profile }: UserProfileClientProps) {
|
|||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{section.type === 'courses' && (
|
||||
<div>
|
||||
{isLoadingCourses ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
|
||||
</div>
|
||||
) : userCourses.length > 0 ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 auto-rows-fr">
|
||||
{userCourses.map((course) => (
|
||||
<div key={course.id} className="flex">
|
||||
<CourseThumbnailLanding
|
||||
course={course}
|
||||
orgslug={userData.org_slug || course.org_slug}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
No courses found
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react'
|
||||
import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd'
|
||||
import { Plus, Trash2, GripVertical, ImageIcon, Link as LinkIcon, Award, ArrowRight, Edit, TextIcon, Briefcase, GraduationCap, Upload, MapPin } from 'lucide-react'
|
||||
import { Plus, Trash2, GripVertical, ImageIcon, Link as LinkIcon, Award, ArrowRight, Edit, TextIcon, Briefcase, GraduationCap, Upload, MapPin, BookOpen } from 'lucide-react'
|
||||
import { Input } from "@components/ui/input"
|
||||
import { Textarea } from "@components/ui/textarea"
|
||||
import { Label } from "@components/ui/label"
|
||||
|
|
@ -48,6 +48,11 @@ const SECTION_TYPES = {
|
|||
icon: MapPin,
|
||||
label: 'Affiliation',
|
||||
description: 'Add organizational affiliations'
|
||||
},
|
||||
'courses': {
|
||||
icon: BookOpen,
|
||||
label: 'Courses',
|
||||
description: 'Display authored courses'
|
||||
}
|
||||
} as const
|
||||
|
||||
|
|
@ -94,6 +99,14 @@ interface ProfileAffiliation {
|
|||
logoUrl: string;
|
||||
}
|
||||
|
||||
interface Course {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
thumbnail?: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
interface BaseSection {
|
||||
id: string;
|
||||
type: keyof typeof SECTION_TYPES;
|
||||
|
|
@ -135,6 +148,11 @@ interface AffiliationSection extends BaseSection {
|
|||
affiliations: ProfileAffiliation[];
|
||||
}
|
||||
|
||||
interface CoursesSection extends BaseSection {
|
||||
type: 'courses';
|
||||
// No need to store courses as they will be fetched from API
|
||||
}
|
||||
|
||||
type ProfileSection =
|
||||
| ImageGallerySection
|
||||
| TextSection
|
||||
|
|
@ -142,7 +160,8 @@ type ProfileSection =
|
|||
| SkillsSection
|
||||
| ExperienceSection
|
||||
| EducationSection
|
||||
| AffiliationSection;
|
||||
| AffiliationSection
|
||||
| CoursesSection;
|
||||
|
||||
interface ProfileData {
|
||||
sections: ProfileSection[];
|
||||
|
|
@ -196,7 +215,7 @@ const UserProfileBuilder = () => {
|
|||
const baseSection = {
|
||||
id: `section-${Date.now()}`,
|
||||
type,
|
||||
title: `New ${SECTION_TYPES[type].label} Section`
|
||||
title: `${SECTION_TYPES[type].label} Section`
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
|
|
@ -242,6 +261,11 @@ const UserProfileBuilder = () => {
|
|||
type: 'affiliation',
|
||||
affiliations: []
|
||||
}
|
||||
case 'courses':
|
||||
return {
|
||||
...baseSection,
|
||||
type: 'courses'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -507,6 +531,8 @@ const SectionEditor: React.FC<SectionEditorProps> = ({ section, onChange }) => {
|
|||
return <EducationEditor section={section} onChange={onChange} />
|
||||
case 'affiliation':
|
||||
return <AffiliationEditor section={section} onChange={onChange} />
|
||||
case 'courses':
|
||||
return <CoursesEditor section={section} onChange={onChange} />
|
||||
default:
|
||||
return <div>Unknown section type</div>
|
||||
}
|
||||
|
|
@ -1285,4 +1311,35 @@ const AffiliationEditor: React.FC<{
|
|||
)
|
||||
}
|
||||
|
||||
const CoursesEditor: React.FC<{
|
||||
section: CoursesSection;
|
||||
onChange: (section: CoursesSection) => void;
|
||||
}> = ({ section, onChange }) => {
|
||||
return (
|
||||
<div className="space-y-6 p-6 bg-white rounded-lg nice-shadow">
|
||||
<div className="flex items-center space-x-2">
|
||||
<BookOpen className="w-5 h-5 text-gray-500" />
|
||||
<h3 className="font-medium text-lg">Courses</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Title */}
|
||||
<div>
|
||||
<Label htmlFor="title">Section Title</Label>
|
||||
<Input
|
||||
id="title"
|
||||
value={section.title}
|
||||
onChange={(e) => onChange({ ...section, title: e.target.value })}
|
||||
placeholder="Enter section title"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-gray-500 italic">
|
||||
Your authored courses will be automatically displayed in this section.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserProfileBuilder
|
||||
|
|
@ -25,6 +25,14 @@ export async function getUserByUsername(username: string, access_token?: string)
|
|||
return res
|
||||
}
|
||||
|
||||
export async function getCoursesByUser(user_id: string, access_token?: string) {
|
||||
const result = await fetch(
|
||||
`${getAPIUrl()}users/${user_id}/courses`,
|
||||
access_token ? RequestBodyWithAuthHeader('GET', null, null, access_token) : RequestBody('GET', null, null)
|
||||
)
|
||||
const res = await getResponseMetadata(result)
|
||||
return res
|
||||
}
|
||||
export async function updateUserAvatar(
|
||||
user_uuid: any,
|
||||
avatar_file: any,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue