feat: user profile customization: username, password, and avatar (#315)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
import { Button, DropdownTrigger, Dropdown, DropdownMenu, DropdownItem } from '@heroui/react';
|
||||
import { ChevronDown, PenTool, ArrowRightFromLine, ArrowRightToLine } from 'lucide-react';
|
||||
import { ChevronDown, PenTool, ArrowRightFromLine, ArrowRightToLine, Settings } from 'lucide-react';
|
||||
import { useContext } from 'react';
|
||||
import { TokenContext } from '@/utils/TokenProvider';
|
||||
import { useRouter } from '@/src/i18n/routing';
|
||||
@@ -31,12 +31,23 @@ export default function DropdownAccount({ messages, locale, onItemPress }: Props
|
||||
{
|
||||
uid: 'account',
|
||||
title: messages.account,
|
||||
icon: <UserAvatar context={context} />,
|
||||
icon: (
|
||||
<UserAvatar size={16} username={context.token?.user?.username} avatarPath={context.token?.user?.avatarPath} />
|
||||
),
|
||||
onPress: () => {
|
||||
router.push('/account', { locale: locale });
|
||||
onItemPress();
|
||||
},
|
||||
},
|
||||
{
|
||||
uid: 'profile',
|
||||
title: messages.profileSettings,
|
||||
icon: <Settings size={16} />,
|
||||
onPress: () => {
|
||||
router.push('/account/settings', { locale: locale });
|
||||
onItemPress();
|
||||
},
|
||||
},
|
||||
{
|
||||
uid: 'signout',
|
||||
title: messages.signOut,
|
||||
@@ -75,7 +86,13 @@ export default function DropdownAccount({ messages, locale, onItemPress }: Props
|
||||
<Button
|
||||
size="sm"
|
||||
variant="light"
|
||||
startContent={<UserAvatar context={context} />}
|
||||
startContent={
|
||||
<UserAvatar
|
||||
size={16}
|
||||
username={context.token?.user?.username}
|
||||
avatarPath={context.token?.user?.avatarPath}
|
||||
/>
|
||||
}
|
||||
endContent={<ChevronDown size={16} />}
|
||||
>
|
||||
{context.isSignedIn() ? context.token?.user?.username : messages.signIn}
|
||||
|
||||
@@ -10,6 +10,7 @@ export default function Header(params: { locale: LocaleCodeType }) {
|
||||
docs: t('docs'),
|
||||
roadmap: t('roadmap'),
|
||||
account: t('account'),
|
||||
profileSettings: t('profile_settings'),
|
||||
signUp: t('signup'),
|
||||
signIn: t('signin'),
|
||||
signOut: t('signout'),
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
ListboxItem,
|
||||
Listbox,
|
||||
} from '@heroui/react';
|
||||
import { ArrowRightFromLine, ArrowRightToLine, File, Globe, MoveUpRight, PenTool } from 'lucide-react';
|
||||
import { ArrowRightFromLine, ArrowRightToLine, File, Globe, MoveUpRight, PenTool, Settings } from 'lucide-react';
|
||||
import DropdownAccount from './DropdownAccount';
|
||||
import DropdownLanguage from './DropdownLanguage';
|
||||
import { ThemeSwitch } from '@/components/ThemeSwitch';
|
||||
@@ -31,6 +31,7 @@ type NabbarMenuMessages = {
|
||||
docs: string;
|
||||
roadmap: string;
|
||||
account: string;
|
||||
profileSettings: string;
|
||||
signUp: string;
|
||||
signIn: string;
|
||||
signOut: string;
|
||||
@@ -185,12 +186,27 @@ export default function HeaderNavbarMenu({ messages, locale }: Props) {
|
||||
<ListboxItem
|
||||
key="account"
|
||||
title={messages.account}
|
||||
startContent={<UserAvatar context={context} />}
|
||||
startContent={
|
||||
<UserAvatar
|
||||
size={16}
|
||||
username={context.token?.user?.username}
|
||||
avatarPath={context.token?.user?.avatarPath}
|
||||
/>
|
||||
}
|
||||
onPress={() => {
|
||||
router.push('/account', { locale: locale });
|
||||
setIsMenuOpen(false);
|
||||
}}
|
||||
/>
|
||||
<ListboxItem
|
||||
key="profile"
|
||||
title={messages.profileSettings}
|
||||
startContent={<Settings size={16} />}
|
||||
onPress={() => {
|
||||
router.push('/account/settings', { locale: locale });
|
||||
setIsMenuOpen(false);
|
||||
}}
|
||||
/>
|
||||
<ListboxItem
|
||||
key="signout"
|
||||
title={messages.signOut}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
'use client';
|
||||
import { useState, useEffect, useContext } from 'react';
|
||||
import { Button, Card, CardHeader, CardFooter } from '@heroui/react';
|
||||
import Avatar from 'boring-avatars';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import { ArrowRight, Settings } from 'lucide-react';
|
||||
import { Link, NextUiLinkClasses } from '@/src/i18n/routing';
|
||||
import { TokenContext } from '@/utils/TokenProvider';
|
||||
import { fetchMyProjects } from '@/utils/projectsControl';
|
||||
import { ProjectType } from '@/types/project';
|
||||
import PublicityChip from '@/components/PublicityChip';
|
||||
import UserAvatar from '@/components/UserAvatar';
|
||||
import { LocaleCodeType } from '@/types/locale';
|
||||
import { logError } from '@/utils/errorHandler';
|
||||
|
||||
@@ -17,6 +17,7 @@ type AccountPageMessages = {
|
||||
private: string;
|
||||
notOwnAnyProjects: string;
|
||||
findProjects: string;
|
||||
profileSettings: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
@@ -51,17 +52,28 @@ export default function AccountPage({ messages, locale }: Props) {
|
||||
<div className="container mx-auto max-w-3xl pt-6 px-6 flex-grow">
|
||||
<div className="w-full p-3 flex items-center justify-between">
|
||||
<Card className="w-[600px]">
|
||||
<CardHeader className="flex gap-6">
|
||||
<Avatar
|
||||
size={48}
|
||||
name={context.token?.user?.username}
|
||||
variant="beam"
|
||||
colors={['#0A0310', '#49007E', '#FF005B', '#FF7D10', '#FFB238']}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<p className="text-xl font-bold">{context.token?.user?.username}</p>
|
||||
<p className="text-lg text-default-500">{context.token?.user?.email}</p>
|
||||
<CardHeader className="flex gap-6 justify-between">
|
||||
<div className="flex gap-6">
|
||||
<UserAvatar
|
||||
size={48}
|
||||
username={context.token?.user?.username}
|
||||
avatarPath={context.token?.user?.avatarPath}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<p className="text-xl font-bold">{context.token?.user?.username}</p>
|
||||
<p className="text-lg text-default-500">{context.token?.user?.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
as={Link}
|
||||
href="/account/settings"
|
||||
locale={locale}
|
||||
variant="flat"
|
||||
size="sm"
|
||||
startContent={<Settings size={16} />}
|
||||
>
|
||||
{messages.profileSettings}
|
||||
</Button>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ export default function Page({ params }: PageType) {
|
||||
private: t('private'),
|
||||
notOwnAnyProjects: t('not_own_any_projects'),
|
||||
findProjects: t('find_projects'),
|
||||
profileSettings: t('profile_settings'),
|
||||
};
|
||||
|
||||
return <AccountPage messages={messages} locale={params.locale as LocaleCodeType} />;
|
||||
|
||||
@@ -0,0 +1,383 @@
|
||||
'use client';
|
||||
import { useState, useContext, useRef } from 'react';
|
||||
import { Button, Input, Card, CardHeader, CardBody, addToast, CardFooter } from '@heroui/react';
|
||||
import { TokenContext } from '@/utils/TokenProvider';
|
||||
import { updateUsername, updatePassword, uploadAvatar, deleteAvatar } from '@/utils/usersControl';
|
||||
import { LocaleCodeType } from '@/types/locale';
|
||||
import { logError } from '@/utils/errorHandler';
|
||||
import UserAvatar from '@/components/UserAvatar';
|
||||
|
||||
type ProfileSettingsPageMessages = {
|
||||
profileSettings: string;
|
||||
changeUsername: string;
|
||||
newUsername: string;
|
||||
updateUsername: string;
|
||||
usernameUpdated: string;
|
||||
changePassword: string;
|
||||
currentPassword: string;
|
||||
newPassword: string;
|
||||
confirmNewPassword: string;
|
||||
updatePassword: string;
|
||||
passwordUpdated: string;
|
||||
changeAvatar: string;
|
||||
uploadAvatar: string;
|
||||
removeAvatar: string;
|
||||
avatarUpdated: string;
|
||||
avatarRemoved: string;
|
||||
maxFileSize5mb: string;
|
||||
onlyImagesAllowed: string;
|
||||
currentPasswordIncorrect: string;
|
||||
updateError: string;
|
||||
invalidPassword: string;
|
||||
passwordNotMatch: string;
|
||||
usernameEmpty: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
messages: ProfileSettingsPageMessages;
|
||||
locale: LocaleCodeType;
|
||||
};
|
||||
|
||||
export default function ProfileSettingsPage({ messages }: Props) {
|
||||
const context = useContext(TokenContext);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [username, setUsername] = useState('');
|
||||
const [currentPassword, setCurrentPassword] = useState('');
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [isUpdatingUsername, setIsUpdatingUsername] = useState(false);
|
||||
const [isUpdatingPassword, setIsUpdatingPassword] = useState(false);
|
||||
const [isUploadingAvatar, setIsUploadingAvatar] = useState(false);
|
||||
|
||||
const handleUsernameUpdate = async () => {
|
||||
if (!username.trim()) {
|
||||
addToast({
|
||||
title: 'Warning',
|
||||
color: 'warning',
|
||||
description: messages.usernameEmpty,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setIsUpdatingUsername(true);
|
||||
try {
|
||||
const result = await updateUsername(context.token.access_token, username);
|
||||
if (result && result.user) {
|
||||
// refresh username
|
||||
const newToken = { ...context.token };
|
||||
if (newToken.user) {
|
||||
newToken.user.username = result.user.username;
|
||||
}
|
||||
context.setToken(newToken);
|
||||
context.storeTokenToLocalStorage(newToken);
|
||||
|
||||
addToast({
|
||||
title: 'Success',
|
||||
color: 'success',
|
||||
description: messages.usernameUpdated,
|
||||
});
|
||||
setUsername('');
|
||||
}
|
||||
} catch (error) {
|
||||
logError('Error updating username:', error);
|
||||
addToast({
|
||||
title: 'Error',
|
||||
color: 'danger',
|
||||
description: messages.updateError,
|
||||
});
|
||||
} finally {
|
||||
setIsUpdatingUsername(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePasswordUpdate = async () => {
|
||||
if (!currentPassword || !newPassword) {
|
||||
addToast({
|
||||
title: 'Warning',
|
||||
color: 'warning',
|
||||
description: messages.updateError,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword.length < 8) {
|
||||
addToast({
|
||||
title: 'Warning',
|
||||
color: 'warning',
|
||||
description: messages.invalidPassword,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
addToast({
|
||||
title: 'Warning',
|
||||
color: 'warning',
|
||||
description: messages.passwordNotMatch,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setIsUpdatingPassword(true);
|
||||
try {
|
||||
await updatePassword(context.token.access_token, currentPassword, newPassword);
|
||||
addToast({
|
||||
title: 'Success',
|
||||
color: 'success',
|
||||
description: messages.passwordUpdated,
|
||||
});
|
||||
setCurrentPassword('');
|
||||
setNewPassword('');
|
||||
setConfirmPassword('');
|
||||
} catch (error) {
|
||||
logError('Error updating password:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : messages.updateError;
|
||||
if (errorMessage.includes('incorrect')) {
|
||||
addToast({
|
||||
title: 'Error',
|
||||
color: 'danger',
|
||||
description: messages.currentPasswordIncorrect,
|
||||
});
|
||||
} else {
|
||||
addToast({
|
||||
title: 'Error',
|
||||
color: 'danger',
|
||||
description: messages.updateError,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setIsUpdatingPassword(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAvatarUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
// Validate file type
|
||||
if (!file.type.startsWith('image/')) {
|
||||
addToast({
|
||||
title: 'Warning',
|
||||
color: 'warning',
|
||||
description: messages.onlyImagesAllowed,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate file size (5MB)
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
addToast({
|
||||
title: 'Warning',
|
||||
color: 'warning',
|
||||
description: messages.maxFileSize5mb,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setIsUploadingAvatar(true);
|
||||
try {
|
||||
const result = await uploadAvatar(context.token.access_token, file);
|
||||
if (result && result.user) {
|
||||
const newToken = { ...context.token };
|
||||
if (newToken.user) {
|
||||
newToken.user = result.user;
|
||||
}
|
||||
context.setToken(newToken);
|
||||
context.storeTokenToLocalStorage(newToken);
|
||||
addToast({
|
||||
title: 'Success',
|
||||
color: 'success',
|
||||
description: messages.avatarUpdated,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logError('Error uploading avatar:', error);
|
||||
addToast({
|
||||
title: 'Error',
|
||||
color: 'danger',
|
||||
description: messages.updateError,
|
||||
});
|
||||
} finally {
|
||||
setIsUploadingAvatar(false);
|
||||
// Reset file input
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleAvatarRemove = async () => {
|
||||
setIsUploadingAvatar(true);
|
||||
try {
|
||||
const result = await deleteAvatar(context.token.access_token);
|
||||
if (result && result.user) {
|
||||
const newToken = { ...context.token };
|
||||
if (newToken.user) {
|
||||
newToken.user = result.user;
|
||||
}
|
||||
context.setToken(newToken);
|
||||
context.storeTokenToLocalStorage(newToken);
|
||||
addToast({
|
||||
title: 'Success',
|
||||
color: 'success',
|
||||
description: messages.avatarRemoved,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logError('Error removing avatar:', error);
|
||||
addToast({
|
||||
title: 'Error',
|
||||
color: 'danger',
|
||||
description: messages.updateError,
|
||||
});
|
||||
} finally {
|
||||
setIsUploadingAvatar(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!context.isSignedIn()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-xl pt-6 px-6 flex-grow">
|
||||
<h1 className="text-2xl font-bold mb-6">{messages.profileSettings}</h1>
|
||||
|
||||
{/* Change Username */}
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
<h2 className="text-large font-semibold">{messages.changeUsername}</h2>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<form>
|
||||
<div className="space-y-4">
|
||||
<Input
|
||||
size="sm"
|
||||
autoComplete="username"
|
||||
label={messages.newUsername}
|
||||
placeholder={context.token?.user?.username || ''}
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</CardBody>
|
||||
<CardFooter className="flex justify-end">
|
||||
<Button
|
||||
color="primary"
|
||||
onPress={handleUsernameUpdate}
|
||||
isLoading={isUpdatingUsername}
|
||||
isDisabled={!username.trim()}
|
||||
size="sm"
|
||||
>
|
||||
{messages.updateUsername}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
{/* Change Password */}
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
<h2 className="text-large font-semibold">{messages.changePassword}</h2>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<form>
|
||||
<div className="space-y-4">
|
||||
{/* hidden username field for accessibility */}
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
autoComplete="username"
|
||||
value={context.token?.user?.username || ''}
|
||||
style={{ display: 'none' }}
|
||||
tabIndex={-1}
|
||||
readOnly
|
||||
/>
|
||||
<Input
|
||||
size="sm"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
label={messages.currentPassword}
|
||||
value={currentPassword}
|
||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
size="sm"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
label={messages.newPassword}
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
size="sm"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
label={messages.confirmNewPassword}
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</CardBody>
|
||||
<CardFooter className="flex justify-end">
|
||||
<Button
|
||||
size="sm"
|
||||
color="primary"
|
||||
onPress={handlePasswordUpdate}
|
||||
isLoading={isUpdatingPassword}
|
||||
isDisabled={!currentPassword || !newPassword || !confirmPassword}
|
||||
>
|
||||
{messages.updatePassword}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
{/* Change Avatar */}
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
<h2 className="text-large font-semibold">{messages.changeAvatar}</h2>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<form>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<UserAvatar
|
||||
size={96}
|
||||
username={context.token?.user?.username}
|
||||
avatarPath={context.token?.user?.avatarPath}
|
||||
/>
|
||||
<div className="text-sm text-gray-500">{messages.maxFileSize5mb}</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleAvatarUpload}
|
||||
className="hidden"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</CardBody>
|
||||
<CardFooter className="flex justify-end">
|
||||
{context.token?.user?.avatarPath && (
|
||||
<Button
|
||||
size="sm"
|
||||
color="danger"
|
||||
className="me-2"
|
||||
onPress={handleAvatarRemove}
|
||||
isLoading={isUploadingAvatar}
|
||||
>
|
||||
{messages.removeAvatar}
|
||||
</Button>
|
||||
)}
|
||||
<Button size="sm" color="primary" onPress={() => fileInputRef.current?.click()} isLoading={isUploadingAvatar}>
|
||||
{messages.uploadAvatar}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
35
frontend/src/app/[locale]/account/settings/page.tsx
Normal file
35
frontend/src/app/[locale]/account/settings/page.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useTranslations } from 'next-intl';
|
||||
import ProfileSettingsPage from './ProfileSettingsPage';
|
||||
import { PageType } from '@/types/base';
|
||||
import { LocaleCodeType } from '@/types/locale';
|
||||
|
||||
export default function Page({ params }: PageType) {
|
||||
const t = useTranslations('Auth');
|
||||
const messages = {
|
||||
profileSettings: t('profile_settings'),
|
||||
changeUsername: t('change_username'),
|
||||
newUsername: t('new_username'),
|
||||
updateUsername: t('update_username'),
|
||||
usernameUpdated: t('username_updated'),
|
||||
changePassword: t('change_password'),
|
||||
currentPassword: t('current_password'),
|
||||
newPassword: t('new_password'),
|
||||
confirmNewPassword: t('confirm_new_password'),
|
||||
updatePassword: t('update_password'),
|
||||
passwordUpdated: t('password_updated'),
|
||||
changeAvatar: t('change_avatar'),
|
||||
uploadAvatar: t('upload_avatar'),
|
||||
removeAvatar: t('remove_avatar'),
|
||||
avatarUpdated: t('avatar_updated'),
|
||||
avatarRemoved: t('avatar_removed'),
|
||||
maxFileSize5mb: t('max_file_size_5mb'),
|
||||
onlyImagesAllowed: t('only_images_allowed'),
|
||||
currentPasswordIncorrect: t('current_password_incorrect'),
|
||||
updateError: t('update_error'),
|
||||
invalidPassword: t('invalid_password'),
|
||||
passwordNotMatch: t('password_not_match'),
|
||||
usernameEmpty: t('username_empty'),
|
||||
};
|
||||
|
||||
return <ProfileSettingsPage messages={messages} locale={params.locale as LocaleCodeType} />;
|
||||
}
|
||||
@@ -14,9 +14,9 @@ import {
|
||||
DropdownItem,
|
||||
} from '@heroui/react';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import Avatar from 'boring-avatars';
|
||||
import { UserType, AdminMessages } from '@/types/user';
|
||||
import { roles } from '@/config/selection';
|
||||
import UserAvatar from '@/components/UserAvatar';
|
||||
|
||||
type Props = {
|
||||
users: UserType[];
|
||||
@@ -62,14 +62,7 @@ export default function UsersTable({ users, myself, onChangeRole, messages }: Pr
|
||||
|
||||
switch (columnKey) {
|
||||
case 'avatar':
|
||||
return (
|
||||
<Avatar
|
||||
size={24}
|
||||
name={user.username}
|
||||
variant="beam"
|
||||
colors={['#0A0310', '#49007E', '#FF005B', '#FF7D10', '#FFB238']}
|
||||
/>
|
||||
);
|
||||
return <UserAvatar size={24} username={user.username} avatarPath={user.avatarPath} />;
|
||||
case 'id':
|
||||
return <span>{cellValue}</span>;
|
||||
case 'email':
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useMemo, useCallback } from 'react';
|
||||
import { Button, Table, TableHeader, TableColumn, TableBody, TableRow, TableCell } from '@heroui/react';
|
||||
import Avatar from 'boring-avatars';
|
||||
import { UserType } from '@/types/user';
|
||||
import { MembersMessages } from '@/types/member';
|
||||
import UserAvatar from '@/components/UserAvatar';
|
||||
|
||||
type Props = {
|
||||
candidates: UserType[];
|
||||
@@ -24,14 +24,7 @@ export default function MembersTable({ candidates, onAddPress, messages }: Props
|
||||
|
||||
switch (columnKey) {
|
||||
case 'avatar':
|
||||
return (
|
||||
<Avatar
|
||||
size={16}
|
||||
name={candidate.username}
|
||||
variant="beam"
|
||||
colors={['#0A0310', '#49007E', '#FF005B', '#FF7D10', '#FFB238']}
|
||||
/>
|
||||
);
|
||||
return <UserAvatar size={24} username={candidate.username} avatarPath={candidate.avatarPath} />;
|
||||
case 'email':
|
||||
return cellValue;
|
||||
case 'username':
|
||||
|
||||
@@ -14,10 +14,11 @@ import {
|
||||
DropdownItem,
|
||||
} from '@heroui/react';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import Avatar from 'boring-avatars';
|
||||
import { MemberType, UserType } from '@/types/user';
|
||||
import { memberRoles } from '@/config/selection';
|
||||
import { MembersMessages } from '@/types/member';
|
||||
import Config from '@/config/config';
|
||||
import UserAvatar from '@/components/UserAvatar';
|
||||
|
||||
type Props = {
|
||||
members: MemberType[];
|
||||
@@ -56,14 +57,7 @@ export default function MembersTable({ members, isDisabled, onChangeRole, onDele
|
||||
|
||||
switch (columnKey) {
|
||||
case 'avatar':
|
||||
return (
|
||||
<Avatar
|
||||
size={24}
|
||||
name={member.User.username}
|
||||
variant="beam"
|
||||
colors={['#0A0310', '#49007E', '#FF005B', '#FF7D10', '#FFB238']}
|
||||
/>
|
||||
);
|
||||
return <UserAvatar size={24} username={member.User.username} avatarPath={member.User.avatarPath} />;
|
||||
case 'email':
|
||||
return member.User.email;
|
||||
case 'username':
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client';
|
||||
import { useState, useEffect, useContext } from 'react';
|
||||
import { Button, Table, TableHeader, TableColumn, TableBody, TableRow, TableCell } from '@heroui/react';
|
||||
import Avatar from 'boring-avatars';
|
||||
import { Pencil, Trash } from 'lucide-react';
|
||||
import { SettingsMessages } from '@/types/settings';
|
||||
import { TokenContext } from '@/utils/TokenProvider';
|
||||
@@ -13,6 +12,7 @@ import ProjectDialog from '@/components/ProjectDialog';
|
||||
import { UserType } from '@/types/user';
|
||||
import { findUser } from '@/utils/usersControl';
|
||||
import { logError } from '@/utils/errorHandler';
|
||||
import UserAvatar from '@/components/UserAvatar';
|
||||
|
||||
type Props = {
|
||||
projectId: string;
|
||||
@@ -130,12 +130,7 @@ export default function SettingsPage({ projectId, messages, projectDialogMessage
|
||||
<TableCell>{messages.projectOwner}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Avatar
|
||||
size={24}
|
||||
name={owner.username}
|
||||
variant="beam"
|
||||
colors={['#0A0310', '#49007E', '#FF005B', '#FF7D10', '#FFB238']}
|
||||
/>
|
||||
<UserAvatar size={24} username={owner.username} avatarPath={owner.avatarPath} />
|
||||
<p className="">{owner.username}</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
Reference in New Issue
Block a user