Add display name support for user management and update related components
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m2s
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m2s
This commit is contained in:
@@ -27,8 +27,8 @@ export default function Navbar({ onMenuClick }) {
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
const initials = user?.name
|
||||
? user.name
|
||||
const initials = (user?.display_name || user?.name)
|
||||
? (user.display_name || user.name)
|
||||
.split(' ')
|
||||
.map(n => n[0])
|
||||
.join('')
|
||||
@@ -72,14 +72,14 @@ export default function Navbar({ onMenuClick }) {
|
||||
)}
|
||||
</div>
|
||||
<span className="hidden md:block text-sm font-medium text-th-text">
|
||||
{user?.name}
|
||||
{user?.display_name || user?.name}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{dropdownOpen && (
|
||||
<div className="absolute right-0 mt-2 w-56 bg-th-card rounded-xl border border-th-border shadow-th-lg overflow-hidden">
|
||||
<div className="px-4 py-3 border-b border-th-border">
|
||||
<p className="text-sm font-medium text-th-text">{user?.name}</p>
|
||||
<p className="text-sm font-medium text-th-text">{user?.display_name || user?.name}</p>
|
||||
<p className="text-xs text-th-text-s">{user?.email}</p>
|
||||
</div>
|
||||
<div className="py-1">
|
||||
|
||||
@@ -106,10 +106,10 @@ export default function Sidebar({ open, onClose }) {
|
||||
className="w-9 h-9 rounded-full flex items-center justify-center text-white text-sm font-bold flex-shrink-0"
|
||||
style={{ backgroundColor: user?.avatar_color || '#6366f1' }}
|
||||
>
|
||||
{user?.name?.[0]?.toUpperCase() || '?'}
|
||||
{(user?.display_name || user?.name)?.[0]?.toUpperCase() || '?'}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium text-th-text truncate">{user?.name}</p>
|
||||
<p className="text-sm font-medium text-th-text truncate">{user?.display_name || user?.name}</p>
|
||||
<p className="text-xs text-th-text-s truncate">{user?.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -28,8 +28,8 @@ export function AuthProvider({ children }) {
|
||||
return res.data.user;
|
||||
}, []);
|
||||
|
||||
const register = useCallback(async (name, email, password) => {
|
||||
const res = await api.post('/auth/register', { name, email, password });
|
||||
const register = useCallback(async (username, displayName, email, password) => {
|
||||
const res = await api.post('/auth/register', { username, display_name: displayName, email, password });
|
||||
if (res.data.needsVerification) {
|
||||
return { needsVerification: true };
|
||||
}
|
||||
|
||||
@@ -72,7 +72,16 @@
|
||||
"verifyFailed": "Verifizierung fehlgeschlagen",
|
||||
"verifyFailedTitle": "Verifizierung fehlgeschlagen",
|
||||
"verifyTokenMissing": "Kein Verifizierungstoken vorhanden.",
|
||||
"emailNotVerified": "E-Mail-Adresse noch nicht verifiziert. Bitte prüfe dein Postfach."
|
||||
"emailNotVerified": "E-Mail-Adresse noch nicht verifiziert. Bitte prüfe dein Postfach.",
|
||||
"username": "Benutzername",
|
||||
"usernamePlaceholder": "z.B. maxmuster",
|
||||
"usernameHint": "Nur Buchstaben, Zahlen, _ und - erlaubt (3–30 Zeichen)",
|
||||
"displayName": "Anzeigename",
|
||||
"displayNamePlaceholder": "Max Mustermann",
|
||||
"usernameTaken": "Benutzername ist bereits vergeben",
|
||||
"usernameInvalid": "Benutzername darf nur Buchstaben, Zahlen, _ und - enthalten (3–30 Zeichen)",
|
||||
"usernameRequired": "Benutzername ist erforderlich",
|
||||
"displayNameRequired": "Anzeigename ist erforderlich"
|
||||
},
|
||||
"home": {
|
||||
"poweredBy": "Powered by BigBlueButton",
|
||||
|
||||
@@ -72,7 +72,16 @@
|
||||
"verifyFailed": "Verification failed",
|
||||
"verifyFailedTitle": "Verification failed",
|
||||
"verifyTokenMissing": "No verification token provided.",
|
||||
"emailNotVerified": "Email not yet verified. Please check your inbox."
|
||||
"emailNotVerified": "Email not yet verified. Please check your inbox.",
|
||||
"username": "Username",
|
||||
"usernamePlaceholder": "e.g. johndoe",
|
||||
"usernameHint": "Letters, numbers, _ and - only (3–30 chars)",
|
||||
"displayName": "Display Name",
|
||||
"displayNamePlaceholder": "John Doe",
|
||||
"usernameTaken": "Username is already taken",
|
||||
"usernameInvalid": "Username may only contain letters, numbers, _ and - (3–30 chars)",
|
||||
"usernameRequired": "Username is required",
|
||||
"displayNameRequired": "Display name is required"
|
||||
},
|
||||
"home": {
|
||||
"poweredBy": "Powered by BigBlueButton",
|
||||
|
||||
@@ -25,7 +25,7 @@ export default function Admin() {
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [showCreateUser, setShowCreateUser] = useState(false);
|
||||
const [creatingUser, setCreatingUser] = useState(false);
|
||||
const [newUser, setNewUser] = useState({ name: '', email: '', password: '', role: 'user' });
|
||||
const [newUser, setNewUser] = useState({ name: '', display_name: '', email: '', password: '', role: 'user' });
|
||||
|
||||
// Branding state
|
||||
const [editAppName, setEditAppName] = useState('');
|
||||
@@ -163,7 +163,7 @@ export default function Admin() {
|
||||
await api.post('/admin/users', newUser);
|
||||
toast.success(t('admin.userCreated'));
|
||||
setShowCreateUser(false);
|
||||
setNewUser({ name: '', email: '', password: '', role: 'user' });
|
||||
setNewUser({ name: '', display_name: '', email: '', password: '', role: 'user' });
|
||||
fetchUsers();
|
||||
} catch (err) {
|
||||
toast.error(err.response?.data?.error || t('admin.userCreateFailed'));
|
||||
@@ -173,7 +173,7 @@ export default function Admin() {
|
||||
};
|
||||
|
||||
const filteredUsers = users.filter(u =>
|
||||
u.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
(u.display_name || u.name).toLowerCase().includes(search.toLowerCase()) ||
|
||||
u.email.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
|
||||
@@ -371,12 +371,12 @@ export default function Admin() {
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
u.name[0]?.toUpperCase()
|
||||
(u.display_name || u.name)[0]?.toUpperCase()
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-th-text">{u.name}</p>
|
||||
<p className="text-xs text-th-text-s">{u.email}</p>
|
||||
<p className="text-sm font-medium text-th-text">{u.display_name || u.name}</p>
|
||||
<p className="text-xs text-th-text-s">@{u.name} · {u.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@@ -490,7 +490,7 @@ export default function Admin() {
|
||||
<h3 className="text-lg font-semibold text-th-text mb-4">{t('admin.createUserTitle')}</h3>
|
||||
<form onSubmit={handleCreateUser} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-th-text mb-1.5">{t('auth.name')}</label>
|
||||
<label className="block text-sm font-medium text-th-text mb-1.5">{t('auth.username')}</label>
|
||||
<div className="relative">
|
||||
<User size={18} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-th-text-s" />
|
||||
<input
|
||||
@@ -498,10 +498,24 @@ export default function Admin() {
|
||||
value={newUser.name}
|
||||
onChange={e => setNewUser({ ...newUser, name: e.target.value })}
|
||||
className="input-field pl-11"
|
||||
placeholder={t('auth.namePlaceholder')}
|
||||
placeholder={t('auth.usernamePlaceholder')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-th-text-s mt-1">{t('auth.usernameHint')}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-th-text mb-1.5">{t('auth.displayName')}</label>
|
||||
<div className="relative">
|
||||
<User size={18} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-th-text-s" />
|
||||
<input
|
||||
type="text"
|
||||
value={newUser.display_name}
|
||||
onChange={e => setNewUser({ ...newUser, display_name: e.target.value })}
|
||||
className="input-field pl-11"
|
||||
placeholder={t('auth.displayNamePlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-th-text mb-1.5">{t('auth.email')}</label>
|
||||
|
||||
@@ -7,7 +7,8 @@ import BrandLogo from '../components/BrandLogo';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export default function Register() {
|
||||
const [name, setName] = useState('');
|
||||
const [username, setUsername] = useState('');
|
||||
const [displayName, setDisplayName] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
@@ -32,7 +33,7 @@ export default function Register() {
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await register(name, email, password);
|
||||
const result = await register(username, displayName, email, password);
|
||||
if (result?.needsVerification) {
|
||||
setNeedsVerification(true);
|
||||
toast.success(t('auth.verificationSent'));
|
||||
@@ -87,15 +88,31 @@ export default function Register() {
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-th-text mb-1.5">{t('auth.name')}</label>
|
||||
<label className="block text-sm font-medium text-th-text mb-1.5">{t('auth.username')}</label>
|
||||
<div className="relative">
|
||||
<User size={18} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-th-text-s" />
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
value={username}
|
||||
onChange={e => setUsername(e.target.value)}
|
||||
className="input-field pl-11"
|
||||
placeholder={t('auth.namePlaceholder')}
|
||||
placeholder={t('auth.usernamePlaceholder')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-th-text-s mt-1">{t('auth.usernameHint')}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-th-text mb-1.5">{t('auth.displayName')}</label>
|
||||
<div className="relative">
|
||||
<User size={18} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-th-text-s" />
|
||||
<input
|
||||
type="text"
|
||||
value={displayName}
|
||||
onChange={e => setDisplayName(e.target.value)}
|
||||
className="input-field pl-11"
|
||||
placeholder={t('auth.displayNamePlaceholder')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -574,11 +574,11 @@ export default function RoomDetail() {
|
||||
{u.avatar_image ? (
|
||||
<img src={`${api.defaults.baseURL}/auth/avatar/${u.avatar_image}`} alt="" className="w-full h-full object-cover" />
|
||||
) : (
|
||||
u.name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)
|
||||
(u.display_name || u.name).split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-medium text-th-text truncate">{u.name}</div>
|
||||
<div className="text-sm font-medium text-th-text truncate">{u.display_name || u.name}</div>
|
||||
<div className="text-xs text-th-text-s truncate">{u.email}</div>
|
||||
</div>
|
||||
</button>
|
||||
@@ -600,11 +600,11 @@ export default function RoomDetail() {
|
||||
{u.avatar_image ? (
|
||||
<img src={`${api.defaults.baseURL}/auth/avatar/${u.avatar_image}`} alt="" className="w-full h-full object-cover" />
|
||||
) : (
|
||||
u.name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)
|
||||
(u.display_name || u.name).split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-medium text-th-text truncate">{u.name}</div>
|
||||
<div className="text-sm font-medium text-th-text truncate">{u.display_name || u.name}</div>
|
||||
<div className="text-xs text-th-text-s truncate">{u.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,7 @@ export default function Settings() {
|
||||
|
||||
const [profile, setProfile] = useState({
|
||||
name: user?.name || '',
|
||||
display_name: user?.display_name || '',
|
||||
email: user?.email || '',
|
||||
});
|
||||
const [passwords, setPasswords] = useState({
|
||||
@@ -52,6 +53,7 @@ export default function Settings() {
|
||||
try {
|
||||
const res = await api.put('/auth/profile', {
|
||||
name: profile.name,
|
||||
display_name: profile.display_name,
|
||||
email: profile.email,
|
||||
theme,
|
||||
language,
|
||||
@@ -190,7 +192,7 @@ export default function Settings() {
|
||||
className="w-16 h-16 rounded-full flex items-center justify-center text-white text-xl font-bold"
|
||||
style={{ backgroundColor: user?.avatar_color || '#6366f1' }}
|
||||
>
|
||||
{user?.name?.[0]?.toUpperCase() || '?'}
|
||||
{(user?.display_name || user?.name)?.[0]?.toUpperCase() || '?'}
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
@@ -259,7 +261,7 @@ export default function Settings() {
|
||||
|
||||
<form onSubmit={handleProfileSave} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-th-text mb-1.5">{t('auth.name')}</label>
|
||||
<label className="block text-sm font-medium text-th-text mb-1.5">{t('auth.username')}</label>
|
||||
<div className="relative">
|
||||
<User size={18} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-th-text-s" />
|
||||
<input
|
||||
@@ -270,6 +272,19 @@ export default function Settings() {
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-th-text-s mt-1">{t('auth.usernameHint')}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-th-text mb-1.5">{t('auth.displayName')}</label>
|
||||
<div className="relative">
|
||||
<User size={18} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-th-text-s" />
|
||||
<input
|
||||
type="text"
|
||||
value={profile.display_name}
|
||||
onChange={e => setProfile({ ...profile, display_name: e.target.value })}
|
||||
className="input-field pl-11"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-th-text mb-1.5">{t('auth.email')}</label>
|
||||
|
||||
Reference in New Issue
Block a user