mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: Implement UserProfiles
This commit is contained in:
parent
1bbb0269a3
commit
3b5c4f9d92
14 changed files with 1729 additions and 19 deletions
|
|
@ -28,6 +28,7 @@ from src.services.users.users import (
|
||||||
get_user_session,
|
get_user_session,
|
||||||
read_user_by_id,
|
read_user_by_id,
|
||||||
read_user_by_uuid,
|
read_user_by_uuid,
|
||||||
|
read_user_by_username,
|
||||||
update_user,
|
update_user,
|
||||||
update_user_avatar,
|
update_user_avatar,
|
||||||
update_user_password,
|
update_user_password,
|
||||||
|
|
@ -170,6 +171,20 @@ async def api_get_user_by_uuid(
|
||||||
return await read_user_by_uuid(request, db_session, current_user, user_uuid)
|
return await read_user_by_uuid(request, db_session, current_user, user_uuid)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/username/{username}", response_model=UserRead, tags=["users"])
|
||||||
|
async def api_get_user_by_username(
|
||||||
|
*,
|
||||||
|
request: Request,
|
||||||
|
db_session: Session = Depends(get_db_session),
|
||||||
|
current_user: PublicUser = Depends(get_current_user),
|
||||||
|
username: str,
|
||||||
|
) -> UserRead:
|
||||||
|
"""
|
||||||
|
Get User by Username
|
||||||
|
"""
|
||||||
|
return await read_user_by_username(request, db_session, current_user, username)
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{user_id}", response_model=UserRead, tags=["users"])
|
@router.put("/{user_id}", response_model=UserRead, tags=["users"])
|
||||||
async def api_update_user(
|
async def api_update_user(
|
||||||
*,
|
*,
|
||||||
|
|
|
||||||
|
|
@ -424,6 +424,30 @@ async def read_user_by_uuid(
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
async def read_user_by_username(
|
||||||
|
request: Request,
|
||||||
|
db_session: Session,
|
||||||
|
current_user: PublicUser | AnonymousUser,
|
||||||
|
username: str,
|
||||||
|
):
|
||||||
|
# Get user
|
||||||
|
statement = select(User).where(User.username == username)
|
||||||
|
user = db_session.exec(statement).first()
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="User does not exist",
|
||||||
|
)
|
||||||
|
|
||||||
|
# RBAC check
|
||||||
|
await rbac_check(request, current_user, "read", user.user_uuid, db_session)
|
||||||
|
|
||||||
|
user = UserRead.model_validate(user)
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
async def get_user_session(
|
async def get_user_session(
|
||||||
request: Request,
|
request: Request,
|
||||||
db_session: Session,
|
db_session: Session,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,234 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import UserAvatar from '@components/Objects/UserAvatar'
|
||||||
|
import {
|
||||||
|
Briefcase,
|
||||||
|
Building2,
|
||||||
|
MapPin,
|
||||||
|
Globe,
|
||||||
|
Link as LinkIcon,
|
||||||
|
GraduationCap,
|
||||||
|
Award,
|
||||||
|
BookOpen,
|
||||||
|
Laptop2,
|
||||||
|
Users,
|
||||||
|
Calendar,
|
||||||
|
Lightbulb
|
||||||
|
} from 'lucide-react'
|
||||||
|
import { getUserAvatarMediaDirectory } from '@services/media/media'
|
||||||
|
|
||||||
|
interface UserProfileClientProps {
|
||||||
|
userData: any;
|
||||||
|
profile: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ICON_MAP = {
|
||||||
|
'briefcase': Briefcase,
|
||||||
|
'graduation-cap': GraduationCap,
|
||||||
|
'map-pin': MapPin,
|
||||||
|
'building-2': Building2,
|
||||||
|
'speciality': Lightbulb,
|
||||||
|
'globe': Globe,
|
||||||
|
'laptop-2': Laptop2,
|
||||||
|
'award': Award,
|
||||||
|
'book-open': BookOpen,
|
||||||
|
'link': LinkIcon,
|
||||||
|
'users': Users,
|
||||||
|
'calendar': Calendar,
|
||||||
|
} as const
|
||||||
|
|
||||||
|
function UserProfileClient({ userData, profile }: UserProfileClientProps) {
|
||||||
|
const IconComponent = ({ iconName }: { iconName: string }) => {
|
||||||
|
const IconElement = ICON_MAP[iconName as keyof typeof ICON_MAP]
|
||||||
|
if (!IconElement) return null
|
||||||
|
return <IconElement className="w-4 h-4 text-gray-600" />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto py-8">
|
||||||
|
{/* Banner */}
|
||||||
|
<div className="h-48 w-full bg-gray-100 rounded-t-xl mb-0 relative overflow-hidden">
|
||||||
|
{/* Optional banner content */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Profile Content */}
|
||||||
|
<div className="bg-white rounded-b-xl nice-shadow p-8 relative">
|
||||||
|
{/* Avatar Positioned on the banner */}
|
||||||
|
<div className="absolute -top-24 left-8">
|
||||||
|
<div className="rounded-xl overflow-hidden shadow-lg border-4 border-white">
|
||||||
|
<UserAvatar
|
||||||
|
width={150}
|
||||||
|
avatar_url={userData.avatar_image ? getUserAvatarMediaDirectory(userData.user_uuid, userData.avatar_image) : ''}
|
||||||
|
predefined_avatar={userData.avatar_image ? undefined : 'empty'}
|
||||||
|
userId={userData.id}
|
||||||
|
showProfilePopup
|
||||||
|
rounded="rounded-xl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Affiliation Logos */}
|
||||||
|
<div className="absolute -top-12 right-8 flex items-center gap-4">
|
||||||
|
{profile.sections?.map((section: any) => (
|
||||||
|
section.type === 'affiliation' && section.affiliations?.map((affiliation: any, index: number) => (
|
||||||
|
affiliation.logoUrl && (
|
||||||
|
<div key={index} className="bg-white rounded-lg p-2 shadow-lg border-2 border-white">
|
||||||
|
<img
|
||||||
|
src={affiliation.logoUrl}
|
||||||
|
alt={affiliation.name}
|
||||||
|
className="w-16 h-16 object-contain"
|
||||||
|
title={affiliation.name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
))
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Profile Content with right padding to avoid overlap */}
|
||||||
|
<div className="mt-20 md:mt-14">
|
||||||
|
<div className="flex flex-col md:flex-row gap-12">
|
||||||
|
{/* Left column with details - aligned with avatar */}
|
||||||
|
<div className="w-full md:w-1/6 pl-2">
|
||||||
|
{/* Name */}
|
||||||
|
<h1 className="text-[32px] font-bold mb-8">
|
||||||
|
{userData.first_name} {userData.last_name}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{/* Details */}
|
||||||
|
<div className="flex flex-col space-y-3">
|
||||||
|
{userData.details && Object.values(userData.details).map((detail: any) => (
|
||||||
|
<div key={detail.id} className="flex items-center gap-4">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<IconComponent iconName={detail.icon} />
|
||||||
|
</div>
|
||||||
|
<span className="text-gray-700 text-[15px] font-medium">{detail.text}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right column with about and related content */}
|
||||||
|
<div className="w-full md:w-4/6">
|
||||||
|
<div className="mb-8">
|
||||||
|
<h2 className="text-xl font-semibold mb-4">About</h2>
|
||||||
|
{userData.bio ? (
|
||||||
|
<p className="text-gray-700">{userData.bio}</p>
|
||||||
|
) : (
|
||||||
|
<p className="text-gray-500 italic">No biography provided</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Profile sections from profile builder */}
|
||||||
|
{profile.sections && profile.sections.length > 0 && (
|
||||||
|
<div>
|
||||||
|
{profile.sections.map((section: any, index: number) => (
|
||||||
|
<div key={index} className="mb-8">
|
||||||
|
<h2 className="text-xl font-semibold mb-4">{section.title}</h2>
|
||||||
|
|
||||||
|
{section.type === 'text' && (
|
||||||
|
<div className="prose max-w-none">{section.content}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{section.type === 'links' && (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{section.links.map((link: any, linkIndex: number) => (
|
||||||
|
<a
|
||||||
|
key={linkIndex}
|
||||||
|
href={link.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center space-x-2 text-blue-600 hover:text-blue-800"
|
||||||
|
>
|
||||||
|
<LinkIcon className="w-4 h-4" />
|
||||||
|
<span>{link.title}</span>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{section.type === 'skills' && (
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{section.skills.map((skill: any, skillIndex: number) => (
|
||||||
|
<span
|
||||||
|
key={skillIndex}
|
||||||
|
className="px-3 py-1 bg-gray-100 rounded-full text-sm"
|
||||||
|
>
|
||||||
|
{skill.name}
|
||||||
|
{skill.level && ` • ${skill.level}`}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{section.type === 'experience' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{section.experiences.map((exp: any, expIndex: number) => (
|
||||||
|
<div key={expIndex} className="border-l-2 border-gray-200 pl-4">
|
||||||
|
<h3 className="font-medium">{exp.title}</h3>
|
||||||
|
<p className="text-gray-600">{exp.organization}</p>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
{exp.startDate} - {exp.current ? 'Present' : exp.endDate}
|
||||||
|
</p>
|
||||||
|
{exp.description && (
|
||||||
|
<p className="mt-2 text-gray-700">{exp.description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{section.type === 'education' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{section.education.map((edu: any, eduIndex: number) => (
|
||||||
|
<div key={eduIndex} className="border-l-2 border-gray-200 pl-4">
|
||||||
|
<h3 className="font-medium">{edu.institution}</h3>
|
||||||
|
<p className="text-gray-600">{edu.degree} in {edu.field}</p>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
{edu.startDate} - {edu.current ? 'Present' : edu.endDate}
|
||||||
|
</p>
|
||||||
|
{edu.description && (
|
||||||
|
<p className="mt-2 text-gray-700">{edu.description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{section.type === 'affiliation' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{section.affiliations.map((affiliation: any, affIndex: number) => (
|
||||||
|
<div key={affIndex} className="border-l-2 border-gray-200 pl-4">
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
{affiliation.logoUrl && (
|
||||||
|
<img
|
||||||
|
src={affiliation.logoUrl}
|
||||||
|
alt={affiliation.name}
|
||||||
|
className="w-12 h-12 object-contain"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium">{affiliation.name}</h3>
|
||||||
|
{affiliation.description && (
|
||||||
|
<p className="mt-2 text-gray-700">{affiliation.description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserProfileClient
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { getUserByUsername } from '@services/users/users'
|
||||||
|
import { getServerSession } from 'next-auth'
|
||||||
|
import { nextAuthOptions } from 'app/auth/options'
|
||||||
|
import { Metadata } from 'next'
|
||||||
|
import UserProfileClient from './UserProfileClient'
|
||||||
|
|
||||||
|
interface UserPageParams {
|
||||||
|
username: string;
|
||||||
|
orgslug: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserPageProps {
|
||||||
|
params: Promise<UserPageParams>;
|
||||||
|
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata({ params }: UserPageProps): Promise<Metadata> {
|
||||||
|
try {
|
||||||
|
const session = await getServerSession(nextAuthOptions)
|
||||||
|
const access_token = session?.tokens?.access_token
|
||||||
|
const resolvedParams = await params
|
||||||
|
|
||||||
|
if (!access_token) {
|
||||||
|
return {
|
||||||
|
title: 'User Profile',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const userData = await getUserByUsername(resolvedParams.username, access_token)
|
||||||
|
return {
|
||||||
|
title: `${userData.first_name} ${userData.last_name} | Profile`,
|
||||||
|
description: userData.bio || `Profile page of ${userData.first_name} ${userData.last_name}`,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
title: 'User Profile',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function UserPage({ params }: UserPageProps) {
|
||||||
|
const resolvedParams = await params;
|
||||||
|
const { username } = resolvedParams;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get access token from server session
|
||||||
|
const session = await getServerSession(nextAuthOptions)
|
||||||
|
const access_token = session?.tokens?.access_token
|
||||||
|
|
||||||
|
if (!access_token) {
|
||||||
|
throw new Error('No access token available')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch user data by username
|
||||||
|
const userData = await getUserByUsername(username, access_token);
|
||||||
|
const profile = userData.profile ? (
|
||||||
|
typeof userData.profile === 'string' ? JSON.parse(userData.profile) : userData.profile
|
||||||
|
) : { sections: [] };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<UserProfileClient
|
||||||
|
userData={userData}
|
||||||
|
profile={profile}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching user data:', error)
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto py-8">
|
||||||
|
<div className="bg-white rounded-xl nice-shadow p-6">
|
||||||
|
<p className="text-red-600">Error loading user profile</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserPage
|
||||||
|
|
@ -39,18 +39,18 @@ const navigationItems: NavigationItem[] = [
|
||||||
icon: Info,
|
icon: Info,
|
||||||
component: UserEditGeneral
|
component: UserEditGeneral
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'profile',
|
||||||
|
label: 'Profile',
|
||||||
|
icon: User,
|
||||||
|
component: UserProfile
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'security',
|
id: 'security',
|
||||||
label: 'Password',
|
label: 'Password',
|
||||||
icon: Lock,
|
icon: Lock,
|
||||||
component: UserEditPassword
|
component: UserEditPassword
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'profile',
|
|
||||||
label: 'Profile',
|
|
||||||
icon: User,
|
|
||||||
component: UserProfile
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const SettingsNavigation = ({
|
const SettingsNavigation = ({
|
||||||
|
|
|
||||||
|
|
@ -1400,7 +1400,7 @@ const PeopleSectionEditor: React.FC<{
|
||||||
<Label>People</Label>
|
<Label>People</Label>
|
||||||
<div className="space-y-4 mt-2">
|
<div className="space-y-4 mt-2">
|
||||||
{section.people.map((person, index) => (
|
{section.people.map((person, index) => (
|
||||||
<div key={index} className="grid grid-cols-[1fr_1fr_1fr_auto] gap-4 p-4 border rounded-lg">
|
<div key={index} className="grid grid-cols-[1fr_1fr_1fr_1fr_auto] gap-4 p-4 border rounded-lg">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Name</Label>
|
<Label>Name</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -1414,6 +1414,19 @@ const PeopleSectionEditor: React.FC<{
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Username</Label>
|
||||||
|
<Input
|
||||||
|
value={person.username || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newPeople = [...section.people]
|
||||||
|
newPeople[index] = { ...person, username: e.target.value }
|
||||||
|
onChange({ ...section, people: newPeople })
|
||||||
|
}}
|
||||||
|
placeholder="@username"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Image</Label>
|
<Label>Image</Label>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|
@ -1480,7 +1493,8 @@ const PeopleSectionEditor: React.FC<{
|
||||||
user_uuid: '',
|
user_uuid: '',
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
image_url: ''
|
image_url: '',
|
||||||
|
username: ''
|
||||||
}
|
}
|
||||||
onChange({
|
onChange({
|
||||||
...section,
|
...section,
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ export interface LandingUsers {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
image_url: string;
|
image_url: string;
|
||||||
|
username?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LandingPeople {
|
export interface LandingPeople {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import UserProfileBuilder from './UserProfileBuilder'
|
||||||
|
|
||||||
function UserProfile() {
|
function UserProfile() {
|
||||||
return (
|
return (
|
||||||
<div>UserProfile</div>
|
<div>
|
||||||
|
<UserProfileBuilder />
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -7,6 +7,7 @@ import useSWR from 'swr'
|
||||||
import { getOrgCourses } from '@services/courses/courses'
|
import { getOrgCourses } from '@services/courses/courses'
|
||||||
import { useLHSession } from '@components/Contexts/LHSessionContext'
|
import { useLHSession } from '@components/Contexts/LHSessionContext'
|
||||||
import CourseThumbnailLanding from '@components/Objects/Thumbnails/CourseThumbnailLanding'
|
import CourseThumbnailLanding from '@components/Objects/Thumbnails/CourseThumbnailLanding'
|
||||||
|
import UserAvatar from '@components/Objects/UserAvatar'
|
||||||
|
|
||||||
interface LandingCustomProps {
|
interface LandingCustomProps {
|
||||||
landing: {
|
landing: {
|
||||||
|
|
@ -183,11 +184,21 @@ function LandingCustom({ landing, orgslug }: LandingCustomProps) {
|
||||||
{section.people.map((person, index) => (
|
{section.people.map((person, index) => (
|
||||||
<div key={index} className="w-[140px] flex flex-col items-center">
|
<div key={index} className="w-[140px] flex flex-col items-center">
|
||||||
<div className="w-24 h-24 mb-4">
|
<div className="w-24 h-24 mb-4">
|
||||||
<img
|
{person.username ? (
|
||||||
src={person.image_url}
|
<UserAvatar
|
||||||
alt={person.name}
|
username={person.username}
|
||||||
className="w-full h-full rounded-full object-cover border-4 border-white nice-shadow"
|
width={96}
|
||||||
/>
|
rounded="rounded-full"
|
||||||
|
border="border-4"
|
||||||
|
showProfilePopup
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<img
|
||||||
|
src={person.image_url}
|
||||||
|
alt={person.name}
|
||||||
|
className="w-full h-full rounded-full object-cover border-4 border-white nice-shadow"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg font-semibold text-center text-gray-900">{person.name}</h3>
|
<h3 className="text-lg font-semibold text-center text-gray-900">{person.name}</h3>
|
||||||
<p className="text-sm text-center text-gray-600 mt-1">{person.description}</p>
|
<p className="text-sm text-center text-gray-600 mt-1">{person.description}</p>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import React from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { getUriWithOrg } from '@services/config/config'
|
import { getUriWithOrg } from '@services/config/config'
|
||||||
import { useParams } from 'next/navigation'
|
import { useParams } from 'next/navigation'
|
||||||
import { getUserAvatarMediaDirectory } from '@services/media/media'
|
import { getUserAvatarMediaDirectory } from '@services/media/media'
|
||||||
import { useLHSession } from '@components/Contexts/LHSessionContext'
|
import { useLHSession } from '@components/Contexts/LHSessionContext'
|
||||||
import UserProfilePopup from './UserProfilePopup'
|
import UserProfilePopup from './UserProfilePopup'
|
||||||
|
import { getUserByUsername } from '@services/users/users'
|
||||||
|
|
||||||
type UserAvatarProps = {
|
type UserAvatarProps = {
|
||||||
width?: number
|
width?: number
|
||||||
|
|
@ -16,11 +17,28 @@ type UserAvatarProps = {
|
||||||
backgroundColor?: 'bg-white' | 'bg-gray-100'
|
backgroundColor?: 'bg-white' | 'bg-gray-100'
|
||||||
showProfilePopup?: boolean
|
showProfilePopup?: boolean
|
||||||
userId?: string
|
userId?: string
|
||||||
|
username?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function UserAvatar(props: UserAvatarProps) {
|
function UserAvatar(props: UserAvatarProps) {
|
||||||
const session = useLHSession() as any
|
const session = useLHSession() as any
|
||||||
const params = useParams() as any
|
const params = useParams() as any
|
||||||
|
const [userData, setUserData] = useState<any>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchUserByUsername = async () => {
|
||||||
|
if (props.username && session?.data?.tokens?.access_token) {
|
||||||
|
try {
|
||||||
|
const data = await getUserByUsername(props.username, session.data.tokens.access_token)
|
||||||
|
setUserData(data)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching user by username:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchUserByUsername()
|
||||||
|
}, [props.username, session?.data?.tokens?.access_token])
|
||||||
|
|
||||||
const isExternalUrl = (url: string): boolean => {
|
const isExternalUrl = (url: string): boolean => {
|
||||||
return url.startsWith('http://') || url.startsWith('https://')
|
return url.startsWith('http://') || url.startsWith('https://')
|
||||||
|
|
@ -57,6 +75,17 @@ function UserAvatar(props: UserAvatarProps) {
|
||||||
return props.avatar_url
|
return props.avatar_url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we have user data from username fetch
|
||||||
|
if (userData?.avatar_image) {
|
||||||
|
const avatarUrl = userData.avatar_image
|
||||||
|
// If it's an external URL (e.g., from Google, Facebook, etc.), use it directly
|
||||||
|
if (isExternalUrl(avatarUrl)) {
|
||||||
|
return avatarUrl
|
||||||
|
}
|
||||||
|
// Otherwise, get the local avatar URL
|
||||||
|
return getUserAvatarMediaDirectory(userData.user_uuid, avatarUrl)
|
||||||
|
}
|
||||||
|
|
||||||
// If user has an avatar in session
|
// If user has an avatar in session
|
||||||
if (session?.data?.user?.avatar_image) {
|
if (session?.data?.user?.avatar_image) {
|
||||||
const avatarUrl = session.data.user.avatar_image
|
const avatarUrl = session.data.user.avatar_image
|
||||||
|
|
@ -92,9 +121,9 @@ function UserAvatar(props: UserAvatarProps) {
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
if (props.showProfilePopup && props.userId) {
|
if (props.showProfilePopup && (props.userId || (userData?.id))) {
|
||||||
return (
|
return (
|
||||||
<UserProfilePopup userId={props.userId}>
|
<UserProfilePopup userId={props.userId || userData?.id}>
|
||||||
{avatarImage}
|
{avatarImage}
|
||||||
</UserProfilePopup>
|
</UserProfilePopup>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,7 @@ const UserProfilePopup = ({ children, userId }: UserProfilePopupProps) => {
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-6 w-6 text-gray-600 hover:text-gray-900 flex-shrink-0"
|
className="h-6 w-6 text-gray-600 hover:text-gray-900 flex-shrink-0"
|
||||||
onClick={() => router.push(`/profile/${userId}`)}
|
onClick={() => userData.username && router.push(`/user/${userData.username}`)}
|
||||||
>
|
>
|
||||||
<ExternalLink className="w-4 h-4" />
|
<ExternalLink className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { getAPIUrl } from '@services/config/config'
|
||||||
import {
|
import {
|
||||||
RequestBodyWithAuthHeader,
|
RequestBodyWithAuthHeader,
|
||||||
errorHandling,
|
errorHandling,
|
||||||
|
getResponseMetadata,
|
||||||
} from '@services/utils/ts/requests'
|
} from '@services/utils/ts/requests'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
@ -18,6 +19,6 @@ export async function updateProfile(
|
||||||
`${getAPIUrl()}users/` + user_id,
|
`${getAPIUrl()}users/` + user_id,
|
||||||
RequestBodyWithAuthHeader('PUT', data, null, access_token)
|
RequestBodyWithAuthHeader('PUT', data, null, access_token)
|
||||||
)
|
)
|
||||||
const res = await errorHandling(result)
|
const res = await getResponseMetadata(result)
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,15 @@ export async function getUser(user_id: string, access_token: string) {
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getUserByUsername(username: string, access_token: string) {
|
||||||
|
const result = await fetch(
|
||||||
|
`${getAPIUrl()}users/username/${username}`,
|
||||||
|
RequestBodyWithAuthHeader('GET', null, null, access_token)
|
||||||
|
)
|
||||||
|
const res = await errorHandling(result)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
export async function updateUserAvatar(
|
export async function updateUserAvatar(
|
||||||
user_uuid: any,
|
user_uuid: any,
|
||||||
avatar_file: any,
|
avatar_file: any,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue