first commit
Some checks failed
Build / run (push) Has been cancelled

This commit is contained in:
maher
2025-10-29 11:42:25 +01:00
commit 703f50a09d
4595 changed files with 385164 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
import {UserProfile} from '@app/profile/user-profile';
import clsx from 'clsx';
interface Props {
profile?: UserProfile;
className?: string;
}
export function ProfileDescription({profile, className}: Props) {
if (!profile) return null;
return (
<div className={clsx('text-sm', className)}>
{profile.description && (
<p className="rounded text-secondary whitespace-nowrap overflow-hidden overflow-ellipsis">
{profile.description}
</p>
)}
{profile.city || profile.country ? (
<div className="flex items-center gap-24 justify-between mt-4">
{(profile.city || profile.country) && (
<div className="rounded text-secondary w-max">
{profile.city}
{profile.city && ','} {profile.country}
</div>
)}
</div>
) : null}
</div>
);
}

View File

@@ -0,0 +1,43 @@
import {UserLink} from '@app/profile/user-link';
import {Tooltip} from '@common/ui/tooltip/tooltip';
import {RemoteFavicon} from '@common/ui/remote-favicon';
import clsx from 'clsx';
import {ButtonBase} from '@common/ui/buttons/button-base';
import {OpenInNewIcon} from '@common/icons/material/OpenInNew';
interface Props {
links?: UserLink[];
className?: string;
}
export function ProfileLinks({links, className}: Props) {
if (!links?.length) return null;
if (links.length === 1) {
return (
<a
className="flex items-center max-md:justify-center gap-6 mt-24 md:mt-12 hover:text-primary transition-colors"
href={links[0].url}
>
<OpenInNewIcon className="text-muted" size="sm" />
<span className="capitalize">{links[0].title}</span>
</a>
);
}
return (
<div className={clsx('flex items-center', className)}>
{links.map(link => (
<Tooltip label={link.title} key={link.url}>
<ButtonBase
elementType="a"
href={link.url}
target="_blank"
rel="noreferrer"
>
<RemoteFavicon url={link.url} alt={link.title} size="w-20 h-20" />
</ButtonBase>
</Tooltip>
))}
</div>
);
}

View File

@@ -0,0 +1,69 @@
import {UserAvatar} from '@common/ui/images/user-avatar';
import {ProfileDescription} from '@app/profile/header/profile-description';
import {FollowButton} from '@common/users/follow-button';
import React from 'react';
import {useAuth} from '@common/auth/use-auth';
import {User} from '@common/auth/user';
import {Trans} from '@common/i18n/trans';
import {DialogTrigger} from '@common/ui/overlays/dialog/dialog-trigger';
import {Button} from '@common/ui/buttons/button';
import {EditIcon} from '@common/icons/material/Edit';
import {EditUserProfileDialog} from '@app/profile/edit-user-profile-dialog';
import {ProfileStatsList} from '@app/profile/header/profile-stats-list';
import {ProfileLinks} from '@app/profile/header/profile-links';
import {Chip} from '@common/ui/forms/input-field/chip-field/chip';
interface Props {
user: User;
}
export function ProfilePageHeader({user}: Props) {
const {user: currentUser} = useAuth();
return (
<div className="flex flex-col md:flex-row items-center gap-24">
<UserAvatar user={user} circle size="w-140 h-140" />
<div className="flex-auto">
<div className="flex items-center mb-8 gap-8">
<h1 className="text-2xl font-bold">{user.display_name}</h1>
{user.is_pro && (
<Chip size="xs" color="primary" radius="rounded" className="mt-2">
<Trans message="PRO" />
</Chip>
)}
</div>
<ProfileDescription profile={user.profile} />
<div className="flex items-center gap-14 mt-12">
{currentUser?.id !== user.id && (
<FollowButton
variant="outline"
color="primary"
size="xs"
user={user}
/>
)}
{currentUser?.id === user.id && <EditButton user={user} />}
</div>
</div>
<div>
<ProfileStatsList user={user} />
<ProfileLinks
links={user.links}
className="flex-shrink-0 ml-auto mt-12"
/>
</div>
</div>
);
}
interface EditButtonProps {
user: User;
}
function EditButton({user}: EditButtonProps) {
return (
<DialogTrigger type="modal">
<Button variant="outline" size="xs" startIcon={<EditIcon />}>
<Trans message="Edit profile" />
</Button>
<EditUserProfileDialog user={user} />
</DialogTrigger>
);
}

View File

@@ -0,0 +1,79 @@
import {User} from '@common/auth/user';
import React, {
Children,
Fragment,
ReactElement,
ReactNode,
useContext,
} from 'react';
import {SiteConfigContext} from '@common/core/settings/site-config-context';
import {Trans} from '@common/i18n/trans';
import {Link} from 'react-router-dom';
import {FormattedNumber} from '@common/i18n/formatted-number';
interface Props {
user: User;
}
export function ProfileStatsList({user}: Props) {
const {
auth: {getUserProfileLink},
} = useContext(SiteConfigContext);
const profileLink = getUserProfileLink!(user);
return (
<StatsItems>
<StatsItem
label={<Trans message="Followers" />}
value={user.followers_count || 0}
link={`${profileLink}/followers`}
/>
<StatsItem
label={<Trans message="Following" />}
value={user.followed_users_count || 0}
link={`${profileLink}/followed-users`}
/>
<StatsItem
label={<Trans message="Lists" />}
value={user.lists_count || 0}
link={`${profileLink}/lists`}
/>
</StatsItems>
);
}
interface StatsItemsProps {
children: ReactNode;
}
function StatsItems(props: StatsItemsProps) {
const children = Children.toArray(props.children);
return (
<div className="flex items-center">
{children.map((child, index) => (
<Fragment key={index}>
{child}
{index < children.length - 1 && (
<div className="mx-10 h-34 w-1 bg-divider" />
)}
</Fragment>
))}
</div>
);
}
interface StatsItemProps {
label: ReactElement;
value: number;
link: string;
}
function StatsItem({label, value, link}: StatsItemProps) {
return (
<Link to={link} className="group block text-center">
<div className="text-lg font-bold">
<FormattedNumber value={value} />
</div>
<div className="text-xs uppercase text-muted transition-colors group-hover:text-primary">
{label}
</div>
</Link>
);
}