added settings page

This commit is contained in:
Ida Dittrich 2025-09-03 21:49:23 +02:00
parent 55c17a5c9e
commit 4d0928f609
10 changed files with 581 additions and 51 deletions

View file

@ -2,16 +2,44 @@ import React, { useState, useEffect, useRef } from 'react'
import { useMsal } from '@azure/msal-react'
import { FaSignOutAlt } from 'react-icons/fa'
import styles from './SidebarStyles/SidebarUser.module.css'
import { useCurrentUser } from '../../hooks/useUsers'
import { useCurrentUser, useUser, User } from '../../hooks/useUsers'
import { SidebarUserProps } from './sidebarTypes';
const SidebarUser: React.FC<SidebarUserProps> = ({ isMinimized = false }) => {
const { instance } = useMsal();
const { user, isLoading, error, logout } = useCurrentUser();
const { user: currentUser, isLoading: currentUserLoading, logout } = useCurrentUser();
const { getUser } = useUser();
// Local state for user data fetched directly via API
const [user, setUser] = useState<User | null>(null);
const [userLoading, setUserLoading] = useState(false);
const [userError, setUserError] = useState<string | null>(null);
const hasLoadedUser = useRef(false);
const [showLogoutMenu, setShowLogoutMenu] = useState(false);
const [isLoggingOut, setIsLoggingOut] = useState(false);
const userSectionRef = useRef<HTMLDivElement>(null);
// Fetch user data directly using the /api/users/{userId} endpoint
const fetchUserData = async () => {
if (!currentUser?.id || hasLoadedUser.current) return;
hasLoadedUser.current = true;
setUserLoading(true);
setUserError(null);
try {
const userData = await getUser(currentUser.id);
setUser(userData);
} catch (error) {
console.error('Failed to fetch user data in sidebar:', error);
setUserError(typeof error === 'string' ? error : 'Failed to load user data');
hasLoadedUser.current = false; // Reset on error to allow retry
} finally {
setUserLoading(false);
}
};
// Function to get initials from full name
const getInitials = (fullName: string): string => {
return fullName
@ -44,6 +72,28 @@ const SidebarUser: React.FC<SidebarUserProps> = ({ isMinimized = false }) => {
}
};
// Fetch user data when currentUser is available
useEffect(() => {
if (currentUser?.id && !hasLoadedUser.current) {
fetchUserData();
}
}, [currentUser?.id]);
// Listen for user updates from settings page
useEffect(() => {
const handleUserUpdate = () => {
hasLoadedUser.current = false; // Reset flag
if (currentUser?.id) {
fetchUserData();
}
};
window.addEventListener('userInfoUpdated', handleUserUpdate);
return () => {
window.removeEventListener('userInfoUpdated', handleUserUpdate);
};
}, [currentUser?.id]);
// Close popup when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
@ -61,7 +111,7 @@ const SidebarUser: React.FC<SidebarUserProps> = ({ isMinimized = false }) => {
};
}, [showLogoutMenu]);
if (isLoading) {
if (currentUserLoading || userLoading) {
return (
<div className={`${styles.user_section} ${isMinimized ? styles.minimized : ''}`}>
<div className={styles.userContainer}>Lädt...</div>
@ -69,7 +119,7 @@ const SidebarUser: React.FC<SidebarUserProps> = ({ isMinimized = false }) => {
);
}
if (error) {
if (userError) {
return (
<div className={`${styles.user_section} ${isMinimized ? styles.minimized : ''}`}>
<div className={styles.userContainer}>Fehler beim Laden des Benutzerprofils</div>

View file

View file

@ -111,7 +111,7 @@ export function useOrgUsers() {
}
};
const updateUser = async (userId: string, userData: UserUpdateData) => {
const updateUser = async (userId: string, userData: User) => {
await request({
url: `/api/users/${userId}`,
method: 'put',
@ -161,7 +161,7 @@ export function useUser() {
});
};
const updateUser = async (userId: string, userData: UserUpdateData): Promise<User> => {
const updateUser = async (userId: string, userData: User): Promise<User> => {
return await request({
url: `/api/users/${userId}`,
method: 'put',

View file

@ -21,6 +21,25 @@ export default {
'settings.theme.dark': 'Dunkel',
'settings.theme.toggle.light': 'Zu hellem Modus wechseln',
'settings.theme.toggle.dark': 'Zu dunklem Modus wechseln',
'settings.userinfo': 'Benutzerinformationen',
'settings.userinfo.description': 'Verwalten Sie Ihre Kontoinformationen',
'settings.userinfo.username': 'Benutzername',
'settings.userinfo.fullname': 'Vollständiger Name',
'settings.userinfo.email': 'E-Mail-Adresse',
'settings.userinfo.language': 'Sprache',
'settings.userinfo.privilege': 'Berechtigungsstufe',
'settings.userinfo.enabled': 'Kontostatus',
'settings.userinfo.auth_authority': 'Authentifizierungsanbieter',
'settings.userinfo.enabled.true': 'Aktiv',
'settings.userinfo.enabled.false': 'Inaktiv',
'settings.userinfo.loading': 'Benutzerinformationen werden geladen...',
'settings.userinfo.error': 'Fehler beim Laden der Benutzerinformationen',
'settings.userinfo.save': 'Änderungen speichern',
'settings.userinfo.saving': 'Speichern...',
'settings.userinfo.success': 'Benutzerinformationen erfolgreich aktualisiert',
'settings.userinfo.update_error': 'Fehler beim Aktualisieren der Benutzerinformationen',
'settings.userinfo.managed_by': 'Verwaltet von {provider}',
'settings.userinfo.managed_note': 'Dieses Feld wird von {provider} verwaltet und kann nicht geändert werden',
// Languages
'language.german': 'Deutsch',
@ -305,6 +324,12 @@ export default {
'files.upload.error': 'Beim Hochladen ist ein Fehler aufgetreten.',
'files.upload.unexpected_error': 'Beim Hochladen ist ein unerwarteter Fehler aufgetreten.',
// Files Page Upload Actions
'files.drop_zone': 'Dateien hier ablegen',
'files.upload_button': 'Dateien hochladen',
'files.uploading_button': 'Wird hochgeladen...',
'files.upload_aria_label': 'Dateien hochladen',
// Files Page
'files.title': 'Dateien',
'files.table.title': 'Dateien',

View file

@ -21,6 +21,25 @@ export default {
'settings.theme.dark': 'Dark',
'settings.theme.toggle.light': 'Switch to light mode',
'settings.theme.toggle.dark': 'Switch to dark mode',
'settings.userinfo': 'User Information',
'settings.userinfo.description': 'Manage your account information',
'settings.userinfo.username': 'Username',
'settings.userinfo.fullname': 'Full Name',
'settings.userinfo.email': 'Email Address',
'settings.userinfo.language': 'Language',
'settings.userinfo.privilege': 'Privilege Level',
'settings.userinfo.enabled': 'Account Status',
'settings.userinfo.auth_authority': 'Authentication Provider',
'settings.userinfo.enabled.true': 'Active',
'settings.userinfo.enabled.false': 'Inactive',
'settings.userinfo.loading': 'Loading user information...',
'settings.userinfo.error': 'Error loading user information',
'settings.userinfo.save': 'Save Changes',
'settings.userinfo.saving': 'Saving...',
'settings.userinfo.success': 'User information updated successfully',
'settings.userinfo.update_error': 'Error updating user information',
'settings.userinfo.managed_by': 'Managed by {provider}',
'settings.userinfo.managed_note': 'This field is managed by {provider} and cannot be changed',
// Languages
'language.german': 'Deutsch',
@ -306,6 +325,12 @@ export default {
'files.upload.error': 'An error occurred while uploading.',
'files.upload.unexpected_error': 'An unexpected error occurred while uploading.',
// Files Page Upload Actions
'files.drop_zone': 'Drop files here',
'files.upload_button': 'Upload Files',
'files.uploading_button': 'Uploading...',
'files.upload_aria_label': 'Upload files',
// Files Page
'files.title': 'Files',
'files.table.title': 'Files',

View file

@ -21,6 +21,25 @@ export default {
'settings.theme.dark': 'Sombre',
'settings.theme.toggle.light': 'Passer en mode clair',
'settings.theme.toggle.dark': 'Passer en mode sombre',
'settings.userinfo': 'Informations utilisateur',
'settings.userinfo.description': 'Gérez vos informations de compte',
'settings.userinfo.username': 'Nom d\'utilisateur',
'settings.userinfo.fullname': 'Nom complet',
'settings.userinfo.email': 'Adresse e-mail',
'settings.userinfo.language': 'Langue',
'settings.userinfo.privilege': 'Niveau de privilège',
'settings.userinfo.enabled': 'Statut du compte',
'settings.userinfo.auth_authority': 'Fournisseur d\'authentification',
'settings.userinfo.enabled.true': 'Actif',
'settings.userinfo.enabled.false': 'Inactif',
'settings.userinfo.loading': 'Chargement des informations utilisateur...',
'settings.userinfo.error': 'Erreur lors du chargement des informations utilisateur',
'settings.userinfo.save': 'Enregistrer les modifications',
'settings.userinfo.saving': 'Enregistrement...',
'settings.userinfo.success': 'Informations utilisateur mises à jour avec succès',
'settings.userinfo.update_error': 'Erreur lors de la mise à jour des informations utilisateur',
'settings.userinfo.managed_by': 'Géré par {provider}',
'settings.userinfo.managed_note': 'Ce champ est géré par {provider} et ne peut pas être modifié',
// Languages
'language.german': 'Deutsch',
@ -306,6 +325,12 @@ export default {
'files.upload.error': 'Une erreur s\'est produite lors du téléchargement.',
'files.upload.unexpected_error': 'Une erreur inattendue s\'est produite lors du téléchargement.',
// Files Page Upload Actions
'files.drop_zone': 'Déposer les fichiers ici',
'files.upload_button': 'Télécharger des fichiers',
'files.uploading_button': 'Téléchargement...',
'files.upload_aria_label': 'Télécharger des fichiers',
// Files Page
'files.title': 'Fichiers',
'files.table.title': 'Fichiers',

View file

@ -74,17 +74,17 @@ function Dateien() {
onClick={triggerFilePicker}
>
<span className={styles.dropZoneText}>
{isDragOver ? 'Drop files here' : 'Drop files here'}
{t('files.drop_zone')}
</span>
</div>
<button
className={sharedStyles.primaryButton}
onClick={triggerFilePicker}
disabled={uploadingFile}
aria-label="Upload files"
aria-label={t('files.upload_aria_label')}
>
<span className={sharedStyles.buttonIcon}><IoMdCloudUpload /></span>
{uploadingFile ? 'Uploading...' : 'Upload Files'}
{uploadingFile ? t('files.uploading_button') : t('files.upload_button')}
</button>
<input
ref={fileInputRef}

View file

@ -1,11 +1,52 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useRef } from 'react';
import styles from './HomeStyles/Einstellungen.module.css';
import sharedStyles from '../../components/PageManager/pages.module.css';
import { useLanguage, Language } from '../../contexts/LanguageContext';
import { useCurrentUser, useUser, User } from '../../hooks/useUsers';
function Einstellungen() {
const [isDarkMode, setIsDarkMode] = useState(false);
const { currentLanguage, setLanguage, t, isLoading } = useLanguage();
const { user: currentUser, isLoading: currentUserLoading } = useCurrentUser();
const { getUser, updateUser, isLoading: updateLoading } = useUser();
// Local state for user data fetched directly via API
const [user, setUser] = useState<User | null>(null);
const [userLoading, setUserLoading] = useState(false);
const [userError, setUserError] = useState<string | null>(null);
// Form state for user info
const [userForm, setUserForm] = useState({
username: '',
fullName: '',
email: '',
language: 'en' as Language,
privilege: '',
enabled: true
});
const [updateMessage, setUpdateMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null);
const [isUpdating, setIsUpdating] = useState(false); // Flag to prevent form reset during update
const hasLoadedUser = useRef(false);
// Fetch user data directly using the /api/users/{userId} endpoint
const fetchUserData = async () => {
if (!currentUser?.id || hasLoadedUser.current) return;
hasLoadedUser.current = true;
setUserLoading(true);
setUserError(null);
try {
const userData = await getUser(currentUser.id);
setUser(userData);
} catch (error) {
console.error('Failed to fetch user data:', error);
setUserError(typeof error === 'string' ? error : 'Failed to load user data');
hasLoadedUser.current = false; // Reset on error to allow retry
} finally {
setUserLoading(false);
}
};
// Sync component state with current theme on mount
useEffect(() => {
@ -13,6 +54,28 @@ function Einstellungen() {
const prefersDark = savedTheme === 'dark' || (!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches);
setIsDarkMode(prefersDark);
}, []);
// Fetch user data when currentUser is available
useEffect(() => {
if (currentUser?.id && !hasLoadedUser.current) {
fetchUserData();
}
}, [currentUser?.id]);
// Update form when user data is loaded (but not during an active update)
useEffect(() => {
if (user && !isUpdating) {
console.log('🔄 Updating form with user data:', user);
setUserForm({
username: user.username || '',
fullName: user.fullName || '',
email: user.email || '',
language: (user.language as Language) || currentLanguage,
privilege: user.privilege || '',
enabled: user.enabled
});
}
}, [user, currentLanguage, isUpdating]);
const applyTheme = (isDark: boolean) => {
if (isDark) {
@ -31,14 +94,93 @@ function Einstellungen() {
applyTheme(newIsDarkMode);
localStorage.setItem('theme', newIsDarkMode ? 'dark' : 'light');
};
const handleLanguageChange = async (language: Language) => {
if (language === currentLanguage) return;
const handleUserFormChange = (field: keyof typeof userForm, value: string | boolean) => {
setUserForm(prev => ({ ...prev, [field]: value }));
setUpdateMessage(null); // Clear any previous messages
// Language change will be handled when the form is submitted, not immediately
};
const handleSaveUserInfo = async () => {
if (!user) return;
setIsUpdating(true); // Prevent form reset during update
try {
await setLanguage(language);
// Create complete User object with updated form data
// Only include editable fields based on authentication authority
const completeUserData: User = {
id: user.id,
username: user.authenticationAuthority === 'local' ? userForm.username : user.username,
fullName: user.authenticationAuthority === 'local' ? userForm.fullName : user.fullName,
email: user.authenticationAuthority === 'local' ? userForm.email : user.email,
language: userForm.language, // Language is always editable
privilege: userForm.privilege,
enabled: userForm.enabled,
authenticationAuthority: user.authenticationAuthority,
mandateId: user.mandateId
};
// Update user via API - this returns the updated user
const updatedUser = await updateUser(user.id, completeUserData);
if (updatedUser) {
console.log('✅ User update successful:', updatedUser);
// Update local user state with the returned data
setUser(updatedUser);
// Update frontend language if it was changed
const newLanguage = updatedUser.language as Language;
if (newLanguage && newLanguage !== currentLanguage) {
try {
await setLanguage(newLanguage);
console.log('🌍 Frontend language updated to:', newLanguage);
} catch (error) {
console.error('Failed to change frontend language:', error);
}
}
// Success: Update form with the actual returned data to ensure consistency
setUserForm({
username: updatedUser.username || '',
fullName: updatedUser.fullName || '',
email: updatedUser.email || '',
language: newLanguage || currentLanguage,
privilege: updatedUser.privilege || '',
enabled: updatedUser.enabled
});
console.log('📝 Form updated with new data');
// Dispatch event to notify other components (like sidebar) that user data was updated
window.dispatchEvent(new CustomEvent('userInfoUpdated'));
setUpdateMessage({ type: 'success', text: t('settings.userinfo.success') });
} else {
throw new Error('No updated user data returned from server');
}
// Clear message after 3 seconds
setTimeout(() => setUpdateMessage(null), 3000);
} catch (error) {
console.error('Failed to change language:', error);
console.error('Failed to update user info:', error);
setUpdateMessage({ type: 'error', text: t('settings.userinfo.update_error') });
// Reset form to original user data on error
if (user) {
setUserForm({
username: user.username || '',
fullName: user.fullName || '',
email: user.email || '',
language: (user.language as Language) || currentLanguage,
privilege: user.privilege || '',
enabled: user.enabled
});
}
} finally {
setIsUpdating(false); // Re-enable form sync
}
};
@ -51,12 +193,12 @@ function Einstellungen() {
}
};
if (isLoading) {
if (isLoading || currentUserLoading || userLoading) {
return (
<div className={sharedStyles.pageContainer}>
<div className={sharedStyles.pageCard}>
<div style={{ padding: '2rem', textAlign: 'center' }}>
{t('common.loading')}
{isLoading ? t('common.loading') : t('settings.userinfo.loading')}
</div>
</div>
</div>
@ -70,6 +212,146 @@ function Einstellungen() {
<div className={sharedStyles.horizontalDivider}></div>
<div className={sharedStyles.contentArea}>
{/* User Information Section */}
{userError && (
<div className={styles.errorMessage}>
{t('settings.userinfo.error')}: {typeof userError === 'string' ? userError : 'An error occurred'}
</div>
)}
{user && (
<div className={styles.userInfoForm}>
<span className={styles.settingLabel}>{t('settings.userinfo')}</span>
<span className={styles.settingDescription}>
{t('settings.userinfo.description')}
</span>
<div className={styles.formRow}>
<div className={styles.formField}>
<label className={styles.fieldLabel}>
{t('settings.userinfo.username')}
{user.authenticationAuthority !== 'local' && (
<span className={styles.fieldNote}>({t('settings.userinfo.managed_by').replace('{provider}', user.authenticationAuthority)})</span>
)}
</label>
<input
type="text"
className={styles.formInput}
value={userForm.username}
onChange={(e) => handleUserFormChange('username', e.target.value)}
placeholder={t('settings.userinfo.username')}
readOnly={user.authenticationAuthority !== 'local'}
title={user.authenticationAuthority !== 'local' ?
t('settings.userinfo.managed_note').replace('{provider}', user.authenticationAuthority) :
undefined}
/>
</div>
<div className={styles.formField}>
<label className={styles.fieldLabel}>
{t('settings.userinfo.fullname')}
{user.authenticationAuthority !== 'local' && (
<span className={styles.fieldNote}>({t('settings.userinfo.managed_by').replace('{provider}', user.authenticationAuthority)})</span>
)}
</label>
<input
type="text"
className={styles.formInput}
value={userForm.fullName}
onChange={(e) => handleUserFormChange('fullName', e.target.value)}
placeholder={t('settings.userinfo.fullname')}
readOnly={user.authenticationAuthority !== 'local'}
title={user.authenticationAuthority !== 'local' ?
t('settings.userinfo.managed_note').replace('{provider}', user.authenticationAuthority) :
undefined}
/>
</div>
</div>
<div className={styles.formRow}>
<div className={styles.formField}>
<label className={styles.fieldLabel}>
{t('settings.userinfo.email')}
{user.authenticationAuthority !== 'local' && (
<span className={styles.fieldNote}>({t('settings.userinfo.managed_by').replace('{provider}', user.authenticationAuthority)})</span>
)}
</label>
<input
type="email"
className={styles.formInput}
value={userForm.email}
onChange={(e) => handleUserFormChange('email', e.target.value)}
placeholder={t('settings.userinfo.email')}
readOnly={user.authenticationAuthority !== 'local'}
title={user.authenticationAuthority !== 'local' ?
t('settings.userinfo.managed_note').replace('{provider}', user.authenticationAuthority) :
undefined}
/>
</div>
<div className={styles.formField}>
<label className={styles.fieldLabel}>
{t('settings.language')}
<span className={styles.fieldNote}>({t('settings.language.description')})</span>
</label>
<select
className={styles.formSelect}
value={userForm.language}
onChange={(e) => handleUserFormChange('language', e.target.value)}
aria-label={t('settings.language')}
>
<option value="de">{getLanguageLabel('de')}</option>
<option value="en">{getLanguageLabel('en')}</option>
<option value="fr">{getLanguageLabel('fr')}</option>
</select>
</div>
</div>
<div className={styles.formRow}>
<div className={styles.formField}>
<label className={styles.fieldLabel}>{t('settings.userinfo.privilege')}</label>
<input
type="text"
className={styles.formInput}
value={userForm.privilege}
readOnly
placeholder={t('settings.userinfo.privilege')}
title="This field is read-only"
/>
</div>
<div className={styles.formField}>
<label className={styles.fieldLabel}>{t('settings.userinfo.auth_authority')}</label>
<input
type="text"
className={styles.formInput}
value={user.authenticationAuthority}
readOnly
placeholder={t('settings.userinfo.auth_authority')}
title="This field is read-only"
/>
</div>
</div>
{updateMessage && (
<div className={`${styles.updateMessage} ${updateMessage.type === 'success' ? styles.successMessage : styles.errorMessage}`}>
{updateMessage.text}
</div>
)}
<div className={styles.formActions}>
<button
className={styles.saveButton}
onClick={handleSaveUserInfo}
disabled={updateLoading}
>
{updateLoading ? t('settings.userinfo.saving') : t('settings.userinfo.save')}
</button>
</div>
</div>
)}
{/* Theme Setting */}
<div className={styles.settingItem}>
<div className={styles.settingInfo}>
<span className={styles.settingLabel}>{t('settings.theme')}</span>
@ -94,35 +376,7 @@ function Einstellungen() {
</button>
</div>
<div className={styles.settingItem}>
<div className={styles.settingInfo}>
<span className={styles.settingLabel}>{t('settings.language')}</span>
<span className={styles.settingDescription}>
{t('settings.language.description')}
</span>
</div>
<select
className={styles.languageSelect}
value={currentLanguage}
onChange={(e) => handleLanguageChange(e.target.value as Language)}
aria-label={t('settings.language')}
>
<option value="de">{getLanguageLabel('de')}</option>
<option value="en">{getLanguageLabel('en')}</option>
<option value="fr">{getLanguageLabel('fr')}</option>
</select>
</div>
<div className={styles.settingsSection}>
<h2 className={styles.sectionTitle}>{t('settings.about')}</h2>
<div className={styles.settingItem}>
<div className={styles.settingInfo}>
<span className={styles.settingLabel}>{t('settings.version')}</span>
<span className={styles.settingDescription}>1.0.0</span>
</div>
</div>
</div>
</div>
</div>
</div>

View file

@ -12,8 +12,8 @@
align-items: center;
padding: 20px;
background: var(--color-bg);
border-radius: 20px;
border: 2px solid var(--color-surface);
border-radius: 25px;
border: 1px solid var(--color-primary);
gap: 20px;
}
@ -44,7 +44,7 @@
gap: 12px;
padding: 12px 20px;
border-radius: 25px;
border: 2px solid var(--color-primary);
border: 1px solid var(--color-primary);
background: var(--color-bg);
color: var(--color-text);
cursor: pointer;
@ -57,8 +57,7 @@
.themeToggle:hover {
border-color: var(--color-secondary);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(63, 81, 181, 0.15);
box-shadow: 0 4px 12px rgba(63, 81, 181, 0.1);
}
@ -123,6 +122,140 @@
padding: 10px;
}
/* User Information Form Styles */
.userInfoForm {
display: flex;
flex-direction: column;
gap: 20px;
padding: 20px;
background: var(--color-bg);
border-radius: 25px;
border: 1px solid var(--color-primary);
margin-bottom: 20px;
}
.formRow {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.formField {
display: flex;
flex-direction: column;
gap: 8px;
flex: 1;
min-width: 250px;
}
.fieldLabel {
font-size: 0.875rem;
font-weight: 500;
color: var(--color-text);
font-family: var(--font-family);
}
.fieldNote {
font-size: 0.75rem;
font-weight: 400;
color: var(--color-primary);
font-style: italic;
}
.formInput,
.formSelect {
padding: 12px 16px;
border-radius: 25px;
border: 1px solid var(--color-primary);
background: var(--color-bg);
color: var(--color-text);
font-family: var(--font-family);
font-size: 0.875rem;
transition: all 0.3s ease;
outline: none;
}
.formInput:focus,
.formSelect:focus {
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(63, 81, 181, 0.1);
}
.formInput:hover,
.formSelect:hover {
border-color: var(--color-secondary);
}
.formInput[readonly] {
background: var(--color-gray-light);
cursor: not-allowed;
opacity: 0.7;
}
.formSelect option {
background: var(--color-bg);
color: var(--color-text);
padding: 10px;
}
.formActions {
display: flex;
justify-content: flex-end;
padding-top: 10px;
}
.saveButton {
padding: 12px 24px;
border-radius: 25px;
border: none;
background: var(--color-secondary);
color: white;
cursor: pointer;
transition: all 0.3s ease;
font-family: var(--font-family);
font-size: 0.875rem;
font-weight: 500;
min-width: 120px;
}
.saveButton:hover:not(:disabled) {
background: var(--color-secondary);
border-color: var(--color-secondary);
box-shadow: 0 4px 12px rgba(63, 81, 181, 0.3);
}
.saveButton:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.updateMessage {
padding: 12px 16px;
border-radius: 12px;
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 10px;
}
.successMessage {
background-color: #e8f5e8;
color: #2e7d32;
border: 1px solid #81c784;
}
.errorMessage {
background-color: #fce4ec;
color: #c2185b;
border: 1px solid #f48fb1;
padding: 12px 16px;
border-radius: 12px;
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 20px;
}
/* Responsive design */
@media (max-width: 768px) {
.einstellungenContainer {
@ -142,4 +275,22 @@
.themeToggle {
align-self: flex-end;
}
.formRow {
flex-direction: column;
gap: 15px;
}
.formField {
min-width: unset;
}
.formActions {
justify-content: center;
}
.saveButton {
width: 100%;
max-width: 200px;
}
}