diff --git a/apps/web/components/Objects/Courses/CourseActions/CoursesActions.tsx b/apps/web/components/Objects/Courses/CourseActions/CoursesActions.tsx
index 62d0293e..49453edb 100644
--- a/apps/web/components/Objects/Courses/CourseActions/CoursesActions.tsx
+++ b/apps/web/components/Objects/Courses/CourseActions/CoursesActions.tsx
@@ -18,6 +18,7 @@ import { useContributorStatus } from '../../../../hooks/useContributorStatus'
interface Author {
user: {
+ id: string
user_uuid: string
avatar_image: string
first_name: string
@@ -66,6 +67,8 @@ const AuthorInfo = ({ author, isMobile }: { author: Author, isMobile: boolean })
avatar_url={author.user.avatar_image ? getUserAvatarMediaDirectory(author.user.user_uuid, author.user.avatar_image) : ''}
predefined_avatar={author.user.avatar_image ? undefined : 'empty'}
width={isMobile ? 60 : 100}
+ showProfilePopup={true}
+ userId={author.user.user_uuid}
/>
Author
@@ -115,6 +118,8 @@ const MultipleAuthors = ({ authors, isMobile }: { authors: Author[], isMobile: b
avatar_url={author.user.avatar_image ? getUserAvatarMediaDirectory(author.user.user_uuid, author.user.avatar_image) : ''}
predefined_avatar={author.user.avatar_image ? undefined : 'empty'}
width={avatarSize}
+ showProfilePopup={true}
+ userId={author.user.id}
/>
diff --git a/apps/web/components/Objects/UserAvatar.tsx b/apps/web/components/Objects/UserAvatar.tsx
index 5ece16be..5d79d92d 100644
--- a/apps/web/components/Objects/UserAvatar.tsx
+++ b/apps/web/components/Objects/UserAvatar.tsx
@@ -3,6 +3,7 @@ import { getUriWithOrg } from '@services/config/config'
import { useParams } from 'next/navigation'
import { getUserAvatarMediaDirectory } from '@services/media/media'
import { useLHSession } from '@components/Contexts/LHSessionContext'
+import UserProfilePopup from './UserProfilePopup'
type UserAvatarProps = {
width?: number
@@ -13,6 +14,8 @@ type UserAvatarProps = {
borderColor?: string
predefined_avatar?: 'ai' | 'empty'
backgroundColor?: 'bg-white' | 'bg-gray-100'
+ showProfilePopup?: boolean
+ userId?: string
}
function UserAvatar(props: UserAvatarProps) {
@@ -69,7 +72,7 @@ function UserAvatar(props: UserAvatarProps) {
return getUriWithOrg(params.orgslug, '/empty_avatar.png')
}
- return (
+ const avatarImage = (
)
+
+ if (props.showProfilePopup && props.userId) {
+ return (
+
+ {avatarImage}
+
+ )
+ }
+
+ return avatarImage
}
export default UserAvatar
diff --git a/apps/web/components/Objects/UserProfilePopup.tsx b/apps/web/components/Objects/UserProfilePopup.tsx
new file mode 100644
index 00000000..f0a48e01
--- /dev/null
+++ b/apps/web/components/Objects/UserProfilePopup.tsx
@@ -0,0 +1,159 @@
+import React, { useEffect, useState } from 'react'
+import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card"
+import { MapPin, Building2, Globe, Briefcase, GraduationCap, Link, Users, Calendar, Lightbulb, Loader2, ExternalLink } from 'lucide-react'
+import { getUser } from '@services/users/users'
+import { useLHSession } from '@components/Contexts/LHSessionContext'
+import { Button } from '@/components/ui/button'
+import { Badge } from '@/components/ui/badge'
+import { useRouter } from 'next/navigation'
+
+type UserProfilePopupProps = {
+ children: React.ReactNode
+ userId: string
+}
+
+type UserData = {
+ first_name: string
+ last_name: string
+ username: string
+ bio?: string
+ avatar_image?: string
+ details?: {
+ [key: string]: {
+ id: string
+ label: string
+ icon: string
+ text: string
+ }
+ }
+}
+
+const ICON_MAP = {
+ 'briefcase': Briefcase,
+ 'graduation-cap': GraduationCap,
+ 'map-pin': MapPin,
+ 'building-2': Building2,
+ 'speciality': Lightbulb,
+ 'globe': Globe,
+ 'link': Link,
+ 'users': Users,
+ 'calendar': Calendar,
+} as const
+
+const UserProfilePopup = ({ children, userId }: UserProfilePopupProps) => {
+ const session = useLHSession() as any
+ const router = useRouter()
+ const [userData, setUserData] = useState(null)
+ const [isLoading, setIsLoading] = useState(false)
+ const [error, setError] = useState(null)
+
+ useEffect(() => {
+ const fetchUserData = async () => {
+ if (!userId) return
+
+ setIsLoading(true)
+ setError(null)
+
+ try {
+ const data = await getUser(userId, session?.data?.tokens?.access_token)
+ setUserData(data)
+ } catch (err) {
+ setError('Failed to load user data')
+ console.error('Error fetching user data:', err)
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ fetchUserData()
+ }, [userId, session?.data?.tokens?.access_token])
+
+ const IconComponent = ({ iconName }: { iconName: string }) => {
+ const IconElement = ICON_MAP[iconName as keyof typeof ICON_MAP]
+ if (!IconElement) return null
+ return
+ }
+
+ return (
+
+
+ {children}
+
+
+ {isLoading ? (
+
+
+
+ ) : error ? (
+ {error}
+ ) : userData ? (
+
+ {/* Header with Avatar and Name */}
+
+ {/* Background gradient */}
+
+
+ {/* Content */}
+
+
+ {/* Avatar */}
+
+
+ {/* Name, Bio, and Button */}
+
+
+
+
+ {userData.first_name} {userData.last_name}
+
+ {userData.username && (
+
+ @{userData.username}
+
+ )}
+
+
+
+ {userData.bio && (
+
+ {userData.bio}
+
+ )}
+
+
+
+
+
+ {/* Details */}
+ {userData.details && Object.values(userData.details).length > 0 && (
+
+ {Object.values(userData.details).map((detail) => (
+
+
+
+ {detail.label}
+ {detail.text}
+
+
+ ))}
+
+ )}
+
+ ) : null}
+
+
+ )
+}
+
+export default UserProfilePopup
\ No newline at end of file
diff --git a/apps/web/components/ui/hover-card.tsx b/apps/web/components/ui/hover-card.tsx
new file mode 100644
index 00000000..e7541864
--- /dev/null
+++ b/apps/web/components/ui/hover-card.tsx
@@ -0,0 +1,44 @@
+"use client"
+
+import * as React from "react"
+import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
+
+import { cn } from "@/lib/utils"
+
+function HoverCard({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function HoverCardTrigger({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function HoverCardContent({
+ className,
+ align = "center",
+ sideOffset = 4,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+export { HoverCard, HoverCardTrigger, HoverCardContent }
diff --git a/apps/web/package.json b/apps/web/package.json
index 9571b82c..25dc1ec1 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -20,6 +20,7 @@
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-form": "^0.0.3",
+ "@radix-ui/react-hover-card": "^1.1.6",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-select": "^2.1.6",
diff --git a/apps/web/pnpm-lock.yaml b/apps/web/pnpm-lock.yaml
index 52bb1b84..46a1eaaf 100644
--- a/apps/web/pnpm-lock.yaml
+++ b/apps/web/pnpm-lock.yaml
@@ -39,6 +39,9 @@ importers:
'@radix-ui/react-form':
specifier: ^0.0.3
version: 0.0.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ '@radix-ui/react-hover-card':
+ specifier: ^1.1.6
+ version: 1.1.6(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@radix-ui/react-icons':
specifier: ^1.3.2
version: 1.3.2(react@19.0.0)
@@ -802,6 +805,19 @@ packages:
'@types/react-dom':
optional: true
+ '@radix-ui/react-hover-card@1.1.6':
+ resolution: {integrity: sha512-E4ozl35jq0VRlrdc4dhHrNSV0JqBb4Jy73WAhBEK7JoYnQ83ED5r0Rb/XdVKw89ReAJN38N492BAPBZQ57VmqQ==}
+ peerDependencies:
+ '@types/react': 19.0.10
+ '@types/react-dom': 19.0.4
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
'@radix-ui/react-icons@1.3.2':
resolution: {integrity: sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==}
peerDependencies:
@@ -4007,6 +4023,23 @@ snapshots:
'@types/react': 19.0.10
'@types/react-dom': 19.0.4(@types/react@19.0.10)
+ '@radix-ui/react-hover-card@1.1.6(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.1
+ '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.10)(react@19.0.0)
+ '@radix-ui/react-context': 1.1.1(@types/react@19.0.10)(react@19.0.0)
+ '@radix-ui/react-dismissable-layer': 1.1.5(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ '@radix-ui/react-popper': 1.2.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ '@radix-ui/react-portal': 1.1.4(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.10)(react@19.0.0)
+ react: 19.0.0
+ react-dom: 19.0.0(react@19.0.0)
+ optionalDependencies:
+ '@types/react': 19.0.10
+ '@types/react-dom': 19.0.4(@types/react@19.0.10)
+
'@radix-ui/react-icons@1.3.2(react@19.0.0)':
dependencies:
react: 19.0.0
@@ -5365,8 +5398,8 @@ snapshots:
'@typescript-eslint/parser': 8.28.0(eslint@8.57.1)(typescript@5.4.4)
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
- eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0)(eslint@8.57.1)
- eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.28.0(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1)
+ eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.28.0(eslint@8.57.1)(typescript@5.4.4))(eslint@8.57.1))(eslint@8.57.1)
+ eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.28.0(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.28.0(eslint@8.57.1)(typescript@5.4.4))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
eslint-plugin-react: 7.37.4(eslint@8.57.1)
eslint-plugin-react-hooks: 5.2.0(eslint@8.57.1)
@@ -5385,7 +5418,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0)(eslint@8.57.1):
+ eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.28.0(eslint@8.57.1)(typescript@5.4.4))(eslint@8.57.1))(eslint@8.57.1):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.4.0
@@ -5396,22 +5429,22 @@ snapshots:
tinyglobby: 0.2.12
unrs-resolver: 1.3.2
optionalDependencies:
- eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.28.0(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1)
+ eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.28.0(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.28.0(eslint@8.57.1)(typescript@5.4.4))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
transitivePeerDependencies:
- supports-color
- eslint-module-utils@2.12.0(@typescript-eslint/parser@8.28.0(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1):
+ eslint-module-utils@2.12.0(@typescript-eslint/parser@8.28.0(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.28.0(eslint@8.57.1)(typescript@5.4.4))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 8.28.0(eslint@8.57.1)(typescript@5.4.4)
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
- eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0)(eslint@8.57.1)
+ eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.28.0(eslint@8.57.1)(typescript@5.4.4))(eslint@8.57.1))(eslint@8.57.1)
transitivePeerDependencies:
- supports-color
- eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.28.0(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1):
+ eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.28.0(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.28.0(eslint@8.57.1)(typescript@5.4.4))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.8
@@ -5422,7 +5455,7 @@ snapshots:
doctrine: 2.1.0
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
- eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.28.0(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1)
+ eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.28.0(eslint@8.57.1)(typescript@5.4.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.28.0(eslint@8.57.1)(typescript@5.4.4))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3