29
resources/client/profile/header/profile-description.tsx
Executable file
29
resources/client/profile/header/profile-description.tsx
Executable 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>
|
||||
);
|
||||
}
|
||||
43
resources/client/profile/header/profile-links.tsx
Executable file
43
resources/client/profile/header/profile-links.tsx
Executable 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>
|
||||
);
|
||||
}
|
||||
69
resources/client/profile/header/profile-page-header.tsx
Executable file
69
resources/client/profile/header/profile-page-header.tsx
Executable 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>
|
||||
);
|
||||
}
|
||||
79
resources/client/profile/header/profile-stats-list.tsx
Executable file
79
resources/client/profile/header/profile-stats-list.tsx
Executable 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user