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 = ( User Avatar ) + + 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 */} +
+
+ {children} +
+
+ + {/* 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