Improve account profile and sidebar details
This commit is contained in:
@@ -28,7 +28,8 @@
|
|||||||
"all": "الكل",
|
"all": "الكل",
|
||||||
"unknownError": "خطأ غير معروف",
|
"unknownError": "خطأ غير معروف",
|
||||||
"confirm": "تأكيد",
|
"confirm": "تأكيد",
|
||||||
"undo": "تراجع"
|
"undo": "تراجع",
|
||||||
|
"reset": "إعادة التعيين للأصل"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"dashboard": "لوحة التحكم",
|
"dashboard": "لوحة التحكم",
|
||||||
|
|||||||
@@ -28,7 +28,8 @@
|
|||||||
"all": "Alle",
|
"all": "Alle",
|
||||||
"unknownError": "Unbekannter Fehler",
|
"unknownError": "Unbekannter Fehler",
|
||||||
"confirm": "Bestätigen",
|
"confirm": "Bestätigen",
|
||||||
"undo": "Rückgängig"
|
"undo": "Rückgängig",
|
||||||
|
"reset": "Auf Original zurücksetzen"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"dashboard": "Übersicht",
|
"dashboard": "Übersicht",
|
||||||
|
|||||||
@@ -28,7 +28,8 @@
|
|||||||
"all": "Όλα",
|
"all": "Όλα",
|
||||||
"unknownError": "Άγνωστο σφάλμα",
|
"unknownError": "Άγνωστο σφάλμα",
|
||||||
"confirm": "Επιβεβαίωση",
|
"confirm": "Επιβεβαίωση",
|
||||||
"undo": "Αναίρεση"
|
"undo": "Αναίρεση",
|
||||||
|
"reset": "Επαναφορά στο αρχικό"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"dashboard": "Επισκόπηση",
|
"dashboard": "Επισκόπηση",
|
||||||
|
|||||||
@@ -28,7 +28,8 @@
|
|||||||
"all": "All",
|
"all": "All",
|
||||||
"unknownError": "Unknown error",
|
"unknownError": "Unknown error",
|
||||||
"confirm": "Confirm",
|
"confirm": "Confirm",
|
||||||
"undo": "Undo"
|
"undo": "Undo",
|
||||||
|
"reset": "Reset to original"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"dashboard": "Overview",
|
"dashboard": "Overview",
|
||||||
|
|||||||
@@ -28,7 +28,8 @@
|
|||||||
"all": "Todo",
|
"all": "Todo",
|
||||||
"unknownError": "Error desconocido",
|
"unknownError": "Error desconocido",
|
||||||
"confirm": "Confirmar",
|
"confirm": "Confirmar",
|
||||||
"undo": "Deshacer"
|
"undo": "Deshacer",
|
||||||
|
"reset": "Restaurar original"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"dashboard": "Inicio",
|
"dashboard": "Inicio",
|
||||||
|
|||||||
@@ -28,7 +28,8 @@
|
|||||||
"all": "Tout",
|
"all": "Tout",
|
||||||
"unknownError": "Erreur inconnue",
|
"unknownError": "Erreur inconnue",
|
||||||
"confirm": "Confirmer",
|
"confirm": "Confirmer",
|
||||||
"undo": "Annuler"
|
"undo": "Annuler",
|
||||||
|
"reset": "Réinitialiser"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"dashboard": "Accueil",
|
"dashboard": "Accueil",
|
||||||
|
|||||||
@@ -28,7 +28,8 @@
|
|||||||
"all": "सभी",
|
"all": "सभी",
|
||||||
"unknownError": "अज्ञात त्रुटि",
|
"unknownError": "अज्ञात त्रुटि",
|
||||||
"confirm": "पुष्टि करें",
|
"confirm": "पुष्टि करें",
|
||||||
"undo": "पूर्ववत करें"
|
"undo": "पूर्ववत करें",
|
||||||
|
"reset": "मूल पर वापस जाएं"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"dashboard": "डैशबोर्ड",
|
"dashboard": "डैशबोर्ड",
|
||||||
|
|||||||
@@ -28,7 +28,8 @@
|
|||||||
"all": "Tutto",
|
"all": "Tutto",
|
||||||
"unknownError": "Errore sconosciuto",
|
"unknownError": "Errore sconosciuto",
|
||||||
"confirm": "Conferma",
|
"confirm": "Conferma",
|
||||||
"undo": "Annulla"
|
"undo": "Annulla",
|
||||||
|
"reset": "Ripristina originale"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"dashboard": "Panoramica",
|
"dashboard": "Panoramica",
|
||||||
|
|||||||
@@ -28,7 +28,8 @@
|
|||||||
"all": "すべて",
|
"all": "すべて",
|
||||||
"unknownError": "不明なエラー",
|
"unknownError": "不明なエラー",
|
||||||
"confirm": "確認",
|
"confirm": "確認",
|
||||||
"undo": "元に戻す"
|
"undo": "元に戻す",
|
||||||
|
"reset": "元に戻す"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"dashboard": "ダッシュボード",
|
"dashboard": "ダッシュボード",
|
||||||
|
|||||||
@@ -28,7 +28,8 @@
|
|||||||
"all": "Todos",
|
"all": "Todos",
|
||||||
"unknownError": "Erro desconhecido",
|
"unknownError": "Erro desconhecido",
|
||||||
"confirm": "Confirmar",
|
"confirm": "Confirmar",
|
||||||
"undo": "Desfazer"
|
"undo": "Desfazer",
|
||||||
|
"reset": "Restaurar original"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"dashboard": "Painel",
|
"dashboard": "Painel",
|
||||||
|
|||||||
@@ -28,7 +28,8 @@
|
|||||||
"all": "Все",
|
"all": "Все",
|
||||||
"unknownError": "Неизвестная ошибка",
|
"unknownError": "Неизвестная ошибка",
|
||||||
"confirm": "Подтвердить",
|
"confirm": "Подтвердить",
|
||||||
"undo": "Отменить"
|
"undo": "Отменить",
|
||||||
|
"reset": "Сбросить к исходному"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"dashboard": "Обзор",
|
"dashboard": "Обзор",
|
||||||
|
|||||||
@@ -28,7 +28,8 @@
|
|||||||
"all": "Alla",
|
"all": "Alla",
|
||||||
"unknownError": "Okänt fel",
|
"unknownError": "Okänt fel",
|
||||||
"confirm": "Bekräfta",
|
"confirm": "Bekräfta",
|
||||||
"undo": "Ångra"
|
"undo": "Ångra",
|
||||||
|
"reset": "Återställ till original"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"dashboard": "Översikt",
|
"dashboard": "Översikt",
|
||||||
|
|||||||
@@ -28,7 +28,8 @@
|
|||||||
"all": "Tümü",
|
"all": "Tümü",
|
||||||
"unknownError": "Bilinmeyen hata",
|
"unknownError": "Bilinmeyen hata",
|
||||||
"confirm": "Onayla",
|
"confirm": "Onayla",
|
||||||
"undo": "Geri al"
|
"undo": "Geri al",
|
||||||
|
"reset": "Orijinale sıfırla"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"dashboard": "Genel Bakış",
|
"dashboard": "Genel Bakış",
|
||||||
|
|||||||
@@ -28,7 +28,8 @@
|
|||||||
"all": "Усі",
|
"all": "Усі",
|
||||||
"unknownError": "Невідома помилка",
|
"unknownError": "Невідома помилка",
|
||||||
"confirm": "Підтвердити",
|
"confirm": "Підтвердити",
|
||||||
"undo": "Скасувати"
|
"undo": "Скасувати",
|
||||||
|
"reset": "Скинути до оригіналу"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"dashboard": "Огляд",
|
"dashboard": "Огляд",
|
||||||
|
|||||||
@@ -28,7 +28,8 @@
|
|||||||
"all": "全部",
|
"all": "全部",
|
||||||
"unknownError": "未知错误",
|
"unknownError": "未知错误",
|
||||||
"confirm": "确认",
|
"confirm": "确认",
|
||||||
"undo": "撤销"
|
"undo": "撤销",
|
||||||
|
"reset": "重置为原始"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"dashboard": "概览",
|
"dashboard": "概览",
|
||||||
|
|||||||
+80
-18
@@ -101,14 +101,17 @@ function avatarHtml(user, className = 'settings-avatar') {
|
|||||||
function avatarEditorHtml(user, prefix) {
|
function avatarEditorHtml(user, prefix) {
|
||||||
return `
|
return `
|
||||||
<div class="settings-avatar-editor">
|
<div class="settings-avatar-editor">
|
||||||
<div class="settings-avatar-preview" id="${prefix}-avatar-preview">
|
<button type="button" class="settings-avatar-button" id="${prefix}-avatar-preview" aria-label="${t('settings.profilePictureLabel')}">
|
||||||
${avatarHtml(user, 'settings-avatar settings-avatar--lg')}
|
${avatarHtml(user, 'settings-avatar settings-avatar--lg')}
|
||||||
</div>
|
</button>
|
||||||
<div class="settings-avatar-editor__controls">
|
<input class="sr-only" type="file" id="${prefix}-avatar-file" accept="image/png,image/jpeg,image/webp" />
|
||||||
<label class="form-label" for="${prefix}-avatar-file">${t('settings.profilePictureLabel')}</label>
|
<div class="settings-avatar-actions">
|
||||||
<input class="form-input" type="file" id="${prefix}-avatar-file" accept="image/png,image/jpeg,image/webp" />
|
<button type="button" class="settings-avatar-action" id="${prefix}-avatar-edit" aria-label="${t('settings.profilePictureLabel')}" title="${t('settings.profilePictureLabel')}">
|
||||||
<p class="form-hint">${t('settings.profilePictureHint')}</p>
|
<i data-lucide="edit-2" aria-hidden="true"></i>
|
||||||
<button type="button" class="btn btn--secondary" id="${prefix}-avatar-remove">${t('settings.profilePictureRemove')}</button>
|
</button>
|
||||||
|
<button type="button" class="settings-avatar-action settings-avatar-action--danger" id="${prefix}-avatar-remove" aria-label="${t('settings.profilePictureRemove')}" title="${t('settings.profilePictureRemove')}">
|
||||||
|
<i data-lucide="trash-2" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -121,6 +124,17 @@ function setAvatarPreview(container, selector, user) {
|
|||||||
preview.insertAdjacentHTML('beforeend', avatarHtml(user, 'settings-avatar settings-avatar--lg'));
|
preview.insertAdjacentHTML('beforeend', avatarHtml(user, 'settings-avatar settings-avatar--lg'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bindAvatarPicker(container, prefix) {
|
||||||
|
const fileInput = container.querySelector(`#${prefix}-avatar-file`);
|
||||||
|
const pickers = [
|
||||||
|
container.querySelector(`#${prefix}-avatar-preview`),
|
||||||
|
container.querySelector(`#${prefix}-avatar-edit`),
|
||||||
|
];
|
||||||
|
pickers.forEach((picker) => {
|
||||||
|
picker?.addEventListener('click', () => fileInput?.click());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function readImageAsDataUrl(file) {
|
function readImageAsDataUrl(file) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!file) return resolve(undefined);
|
if (!file) return resolve(undefined);
|
||||||
@@ -168,6 +182,14 @@ function readImageAsDataUrl(file) {
|
|||||||
* @param {{ user: object }} context
|
* @param {{ user: object }} context
|
||||||
*/
|
*/
|
||||||
export async function render(container, { user }) {
|
export async function render(container, { user }) {
|
||||||
|
try {
|
||||||
|
const me = await auth.me();
|
||||||
|
if (me?.user && user) Object.assign(user, me.user);
|
||||||
|
else if (me?.user) user = me.user;
|
||||||
|
} catch {
|
||||||
|
// Non-critical: render with the user object provided by the router.
|
||||||
|
}
|
||||||
|
|
||||||
// URL-Parameter auswerten (z.B. nach OAuth-Callback)
|
// URL-Parameter auswerten (z.B. nach OAuth-Callback)
|
||||||
const params = new URLSearchParams(location.search);
|
const params = new URLSearchParams(location.search);
|
||||||
const syncOk = params.get('sync_ok');
|
const syncOk = params.get('sync_ok');
|
||||||
@@ -525,18 +547,20 @@ export async function render(container, { user }) {
|
|||||||
<label class="form-label" for="new-username">${t('settings.usernameLabel')}</label>
|
<label class="form-label" for="new-username">${t('settings.usernameLabel')}</label>
|
||||||
<input class="form-input" type="text" id="new-username" required autocomplete="off" />
|
<input class="form-input" type="text" id="new-username" required autocomplete="off" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="settings-name-color-row">
|
||||||
|
<div class="form-group settings-name-color-row__name">
|
||||||
<label class="form-label" for="new-display-name">${t('settings.displayNameLabel')}</label>
|
<label class="form-label" for="new-display-name">${t('settings.displayNameLabel')}</label>
|
||||||
<input class="form-input" type="text" id="new-display-name" required />
|
<input class="form-input" type="text" id="new-display-name" required />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group settings-color-field">
|
||||||
|
<label class="form-label" for="new-avatar-color">${t('settings.colorLabel')}</label>
|
||||||
|
<input class="settings-color-button" type="color" id="new-avatar-color" value="#007AFF" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="new-member-password">${t('settings.memberPasswordLabel')}</label>
|
<label class="form-label" for="new-member-password">${t('settings.memberPasswordLabel')}</label>
|
||||||
<input class="form-input" type="password" id="new-member-password" minlength="8" required autocomplete="new-password" />
|
<input class="form-input" type="password" id="new-member-password" minlength="8" required autocomplete="new-password" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label" for="new-avatar-color">${t('settings.colorLabel')}</label>
|
|
||||||
<input class="form-input form-input--color" type="color" id="new-avatar-color" value="#007AFF" />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="new-family-role">${t('settings.familyRoleLabel')}</label>
|
<label class="form-label" for="new-family-role">${t('settings.familyRoleLabel')}</label>
|
||||||
<select class="form-input" id="new-family-role">
|
<select class="form-input" id="new-family-role">
|
||||||
@@ -626,14 +650,35 @@ export async function render(container, { user }) {
|
|||||||
<div class="settings-card">
|
<div class="settings-card">
|
||||||
<h3 class="settings-card__title">${t('settings.profilePictureTitle')}</h3>
|
<h3 class="settings-card__title">${t('settings.profilePictureTitle')}</h3>
|
||||||
<form id="profile-form" class="settings-form">
|
<form id="profile-form" class="settings-form">
|
||||||
|
<div class="settings-profile-editor">
|
||||||
${avatarEditorHtml(user, 'profile')}
|
${avatarEditorHtml(user, 'profile')}
|
||||||
<div class="form-group">
|
<div class="settings-profile-editor__fields">
|
||||||
|
<div class="settings-name-color-row">
|
||||||
|
<div class="form-group settings-name-color-row__name">
|
||||||
<label class="form-label" for="profile-display-name">${t('settings.displayNameLabel')}</label>
|
<label class="form-label" for="profile-display-name">${t('settings.displayNameLabel')}</label>
|
||||||
<input class="form-input" type="text" id="profile-display-name" maxlength="128" value="${esc(user?.display_name || '')}" required />
|
<input class="form-input" type="text" id="profile-display-name" maxlength="128" value="${esc(user?.display_name || '')}" required />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group settings-color-field">
|
||||||
<label class="form-label" for="profile-avatar-color">${t('settings.colorLabel')}</label>
|
<label class="form-label" for="profile-avatar-color">${t('settings.colorLabel')}</label>
|
||||||
<input class="form-input form-input--color" type="color" id="profile-avatar-color" value="${esc(user?.avatar_color || '#007AFF')}" />
|
<input class="settings-color-button" type="color" id="profile-avatar-color" value="${esc(user?.avatar_color || '#007AFF')}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-grid modal-grid--2">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="profile-phone">${t('settings.memberPhoneLabel')}</label>
|
||||||
|
<input class="form-input" type="tel" id="profile-phone" value="${esc(user?.phone || '')}" autocomplete="tel" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="profile-email">${t('settings.memberEmailLabel')}</label>
|
||||||
|
<input class="form-input" type="email" id="profile-email" value="${esc(user?.email || '')}" autocomplete="email" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="profile-birth-date">${t('settings.memberBirthDateLabel')}</label>
|
||||||
|
<input class="form-input js-date-input" type="text" id="profile-birth-date" value="${esc(formatDateInput(user?.birth_date))}" placeholder="${dateInputPlaceholder()}" inputmode="numeric" />
|
||||||
|
<p class="form-hint">${t('settings.memberContactBirthdayHint')}</p>
|
||||||
</div>
|
</div>
|
||||||
<div id="profile-error" class="form-error" hidden></div>
|
<div id="profile-error" class="form-error" hidden></div>
|
||||||
<div class="settings-form-actions">
|
<div class="settings-form-actions">
|
||||||
@@ -679,6 +724,7 @@ export async function render(container, { user }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bindEvents(container, user, users, categories, icsSubscriptions, apiTokens);
|
bindEvents(container, user, users, categories, icsSubscriptions, apiTokens);
|
||||||
|
if (window.lucide) window.lucide.createIcons();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
@@ -791,6 +837,7 @@ function bindEvents(container, user, users, categories, icsSubscriptions, apiTok
|
|||||||
|
|
||||||
const profileState = { avatarData: user?.avatar_data ?? null };
|
const profileState = { avatarData: user?.avatar_data ?? null };
|
||||||
const profileAvatarFile = container.querySelector('#profile-avatar-file');
|
const profileAvatarFile = container.querySelector('#profile-avatar-file');
|
||||||
|
bindAvatarPicker(container, 'profile');
|
||||||
if (profileAvatarFile) {
|
if (profileAvatarFile) {
|
||||||
profileAvatarFile.addEventListener('change', async () => {
|
profileAvatarFile.addEventListener('change', async () => {
|
||||||
const errorEl = container.querySelector('#profile-error');
|
const errorEl = container.querySelector('#profile-error');
|
||||||
@@ -828,13 +875,21 @@ function bindEvents(container, user, users, categories, icsSubscriptions, apiTok
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const errorEl = container.querySelector('#profile-error');
|
const errorEl = container.querySelector('#profile-error');
|
||||||
const btn = profileForm.querySelector('[type=submit]');
|
const btn = profileForm.querySelector('[type=submit]');
|
||||||
|
const birthDateRaw = container.querySelector('#profile-birth-date')?.value || '';
|
||||||
errorEl.hidden = true;
|
errorEl.hidden = true;
|
||||||
|
if (!isDateInputValid(birthDateRaw)) {
|
||||||
|
showError(errorEl, t('settings.memberBirthDateInvalid'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
try {
|
try {
|
||||||
const res = await auth.updateProfile({
|
const res = await auth.updateProfile({
|
||||||
display_name: container.querySelector('#profile-display-name').value.trim(),
|
display_name: container.querySelector('#profile-display-name').value.trim(),
|
||||||
avatar_color: container.querySelector('#profile-avatar-color').value,
|
avatar_color: container.querySelector('#profile-avatar-color').value,
|
||||||
avatar_data: profileState.avatarData,
|
avatar_data: profileState.avatarData,
|
||||||
|
phone: container.querySelector('#profile-phone')?.value.trim() || null,
|
||||||
|
email: container.querySelector('#profile-email')?.value.trim() || null,
|
||||||
|
birth_date: parseDateInput(birthDateRaw) || null,
|
||||||
});
|
});
|
||||||
Object.assign(user, res.user);
|
Object.assign(user, res.user);
|
||||||
window.oikos?.showToast(t('settings.profileSavedToast'), 'success');
|
window.oikos?.showToast(t('settings.profileSavedToast'), 'success');
|
||||||
@@ -1120,18 +1175,24 @@ function openEditMemberModal(member, currentUser, users, container) {
|
|||||||
size: 'md',
|
size: 'md',
|
||||||
content: `
|
content: `
|
||||||
<form id="edit-member-form" class="settings-form">
|
<form id="edit-member-form" class="settings-form">
|
||||||
|
<div class="settings-profile-editor">
|
||||||
${avatarEditorHtml(member, 'edit-member')}
|
${avatarEditorHtml(member, 'edit-member')}
|
||||||
|
<div class="settings-profile-editor__fields">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="edit-member-username">${t('settings.usernameLabel')}</label>
|
<label class="form-label" for="edit-member-username">${t('settings.usernameLabel')}</label>
|
||||||
<input class="form-input" type="text" id="edit-member-username" value="${esc(member.username)}" required autocomplete="off" />
|
<input class="form-input" type="text" id="edit-member-username" value="${esc(member.username)}" required autocomplete="off" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="settings-name-color-row">
|
||||||
|
<div class="form-group settings-name-color-row__name">
|
||||||
<label class="form-label" for="edit-member-display-name">${t('settings.displayNameLabel')}</label>
|
<label class="form-label" for="edit-member-display-name">${t('settings.displayNameLabel')}</label>
|
||||||
<input class="form-input" type="text" id="edit-member-display-name" value="${esc(member.display_name)}" required maxlength="128" />
|
<input class="form-input" type="text" id="edit-member-display-name" value="${esc(member.display_name)}" required maxlength="128" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group settings-color-field">
|
||||||
<label class="form-label" for="edit-member-avatar-color">${t('settings.colorLabel')}</label>
|
<label class="form-label" for="edit-member-avatar-color">${t('settings.colorLabel')}</label>
|
||||||
<input class="form-input form-input--color" type="color" id="edit-member-avatar-color" value="${esc(member.avatar_color || '#007AFF')}" />
|
<input class="settings-color-button" type="color" id="edit-member-avatar-color" value="${esc(member.avatar_color || '#007AFF')}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="edit-member-family-role">${t('settings.familyRoleLabel')}</label>
|
<label class="form-label" for="edit-member-family-role">${t('settings.familyRoleLabel')}</label>
|
||||||
@@ -1170,6 +1231,7 @@ function openEditMemberModal(member, currentUser, users, container) {
|
|||||||
const fileInput = panel.querySelector('#edit-member-avatar-file');
|
const fileInput = panel.querySelector('#edit-member-avatar-file');
|
||||||
const errorEl = panel.querySelector('#edit-member-error');
|
const errorEl = panel.querySelector('#edit-member-error');
|
||||||
bindSettingsDateInputs(panel);
|
bindSettingsDateInputs(panel);
|
||||||
|
bindAvatarPicker(panel, 'edit-member');
|
||||||
fileInput?.addEventListener('change', async () => {
|
fileInput?.addEventListener('change', async () => {
|
||||||
errorEl.hidden = true;
|
errorEl.hidden = true;
|
||||||
try {
|
try {
|
||||||
|
|||||||
+40
-3
@@ -134,6 +134,7 @@ const PRIMARY_NAV = 4;
|
|||||||
|
|
||||||
const DEFAULT_APP_NAME = 'Oikos';
|
const DEFAULT_APP_NAME = 'Oikos';
|
||||||
const APP_NAME_STORAGE_KEY = 'oikos-app-name';
|
const APP_NAME_STORAGE_KEY = 'oikos-app-name';
|
||||||
|
const APP_VERSION_STORAGE_KEY = 'oikos-app-version';
|
||||||
|
|
||||||
function getDirection(fromPath, toPath) {
|
function getDirection(fromPath, toPath) {
|
||||||
const fromIdx = ROUTE_ORDER.indexOf(fromPath ?? '/');
|
const fromIdx = ROUTE_ORDER.indexOf(fromPath ?? '/');
|
||||||
@@ -146,6 +147,10 @@ function getAppName() {
|
|||||||
return localStorage.getItem(APP_NAME_STORAGE_KEY) || DEFAULT_APP_NAME;
|
return localStorage.getItem(APP_NAME_STORAGE_KEY) || DEFAULT_APP_NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getAppVersion() {
|
||||||
|
return localStorage.getItem(APP_VERSION_STORAGE_KEY) || '';
|
||||||
|
}
|
||||||
|
|
||||||
function setAppName(name) {
|
function setAppName(name) {
|
||||||
const next = String(name || '').trim();
|
const next = String(name || '').trim();
|
||||||
if (next) {
|
if (next) {
|
||||||
@@ -155,6 +160,15 @@ function setAppName(name) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setAppVersion(version) {
|
||||||
|
const next = String(version || '').trim();
|
||||||
|
if (next) {
|
||||||
|
localStorage.setItem(APP_VERSION_STORAGE_KEY, next);
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem(APP_VERSION_STORAGE_KEY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function routeTitle(path) {
|
function routeTitle(path) {
|
||||||
const map = {
|
const map = {
|
||||||
'/': t('dashboard.title'),
|
'/': t('dashboard.title'),
|
||||||
@@ -174,8 +188,14 @@ function routeTitle(path) {
|
|||||||
|
|
||||||
function updateBranding(path = currentPath) {
|
function updateBranding(path = currentPath) {
|
||||||
const appName = getAppName();
|
const appName = getAppName();
|
||||||
const sidebarLogoSpan = document.querySelector('.nav-sidebar__logo span');
|
const sidebarLogoName = document.querySelector('.nav-sidebar__brand-name');
|
||||||
if (sidebarLogoSpan) sidebarLogoSpan.textContent = appName;
|
if (sidebarLogoName) sidebarLogoName.textContent = appName;
|
||||||
|
const sidebarVersion = document.querySelector('.nav-sidebar__version');
|
||||||
|
if (sidebarVersion) {
|
||||||
|
const version = getAppVersion();
|
||||||
|
sidebarVersion.textContent = version ? t('login.version', { version }) : '';
|
||||||
|
sidebarVersion.hidden = !version;
|
||||||
|
}
|
||||||
|
|
||||||
const loginTitle = document.querySelector('.login-hero__title');
|
const loginTitle = document.querySelector('.login-hero__title');
|
||||||
if (path === '/login' && loginTitle) loginTitle.textContent = appName;
|
if (path === '/login' && loginTitle) loginTitle.textContent = appName;
|
||||||
@@ -284,6 +304,14 @@ async function syncPreferencesOnce() {
|
|||||||
} catch {
|
} catch {
|
||||||
// Non-critical. The settings page can refresh this later.
|
// Non-critical. The settings page can refresh this later.
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
const res = await api.get('/version');
|
||||||
|
if (res?.version) setAppVersion(res.version);
|
||||||
|
if (res?.app_name) setAppName(res.app_name);
|
||||||
|
updateBranding();
|
||||||
|
} catch {
|
||||||
|
// Non-critical. The login page and settings page can refresh branding later.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -427,9 +455,18 @@ function renderAppShell(container) {
|
|||||||
logomark.appendChild(logoSvg);
|
logomark.appendChild(logoSvg);
|
||||||
sidebarLogo.appendChild(logomark);
|
sidebarLogo.appendChild(logomark);
|
||||||
|
|
||||||
|
const sidebarBrandText = document.createElement('div');
|
||||||
|
sidebarBrandText.className = 'nav-sidebar__brand-text';
|
||||||
const sidebarLogoSpan = document.createElement('span');
|
const sidebarLogoSpan = document.createElement('span');
|
||||||
|
sidebarLogoSpan.className = 'nav-sidebar__brand-name';
|
||||||
sidebarLogoSpan.textContent = getAppName();
|
sidebarLogoSpan.textContent = getAppName();
|
||||||
sidebarLogo.appendChild(sidebarLogoSpan);
|
const sidebarVersion = document.createElement('small');
|
||||||
|
sidebarVersion.className = 'nav-sidebar__version';
|
||||||
|
const cachedVersion = getAppVersion();
|
||||||
|
sidebarVersion.textContent = cachedVersion ? t('login.version', { version: cachedVersion }) : '';
|
||||||
|
sidebarVersion.hidden = !cachedVersion;
|
||||||
|
sidebarBrandText.append(sidebarLogoSpan, sidebarVersion);
|
||||||
|
sidebarLogo.appendChild(sidebarBrandText);
|
||||||
const sidebarItems = document.createElement('div');
|
const sidebarItems = document.createElement('div');
|
||||||
sidebarItems.className = 'nav-sidebar__items';
|
sidebarItems.className = 'nav-sidebar__items';
|
||||||
sidebarItems.setAttribute('role', 'list');
|
sidebarItems.setAttribute('role', 'list');
|
||||||
|
|||||||
@@ -560,7 +560,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Logo-Text verstecken im collapsed-Modus */
|
/* Logo-Text verstecken im collapsed-Modus */
|
||||||
.nav-sidebar__logo > span {
|
.nav-sidebar__brand-text {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -665,12 +665,29 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-sidebar__logo > span {
|
.nav-sidebar__brand-text {
|
||||||
display: inline-block;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-sidebar__brand-name {
|
||||||
font-size: var(--text-lg);
|
font-size: var(--text-lg);
|
||||||
font-weight: var(--font-weight-bold);
|
font-weight: var(--font-weight-bold);
|
||||||
letter-spacing: -0.3px;
|
letter-spacing: -0.3px;
|
||||||
color: var(--color-text-primary);
|
color: var(--color-text-primary);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-sidebar__version {
|
||||||
|
margin-top: var(--space-0h);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: var(--color-text-tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-sidebar__items {
|
.nav-sidebar__items {
|
||||||
|
|||||||
+113
-8
@@ -10,11 +10,11 @@
|
|||||||
.settings-page { --module-accent: var(--module-settings); }
|
.settings-page { --module-accent: var(--module-settings); }
|
||||||
|
|
||||||
/* --------------------------------------------------------
|
/* --------------------------------------------------------
|
||||||
Seiten-Layout - nutzt layout-center (max 720px)
|
Seiten-Layout
|
||||||
-------------------------------------------------------- */
|
-------------------------------------------------------- */
|
||||||
|
|
||||||
.settings-page {
|
.settings-page {
|
||||||
max-width: var(--content-max-width-narrow);
|
max-width: var(--content-max-width);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,6 +48,7 @@
|
|||||||
|
|
||||||
.settings-tabs {
|
.settings-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
@@ -67,7 +68,7 @@
|
|||||||
|
|
||||||
.settings-tab-btn {
|
.settings-tab-btn {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
padding: var(--space-3) var(--space-4);
|
padding: var(--space-3) clamp(var(--space-2), 1.3vw, var(--space-4));
|
||||||
border: none;
|
border: none;
|
||||||
border-bottom: 2px solid transparent;
|
border-bottom: 2px solid transparent;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -196,23 +197,127 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.settings-avatar-editor {
|
.settings-avatar-editor {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-avatar-button {
|
||||||
|
display: block;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform var(--transition-fast), box-shadow var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-avatar-button:hover,
|
||||||
|
.settings-avatar-button:focus-visible {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-avatar-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-avatar-action {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
background: var(--color-surface);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color var(--transition-fast), border-color var(--transition-fast), background-color var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-avatar-action:hover,
|
||||||
|
.settings-avatar-action:focus-visible {
|
||||||
|
color: var(--color-accent);
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
background: var(--color-accent-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-avatar-action--danger:hover,
|
||||||
|
.settings-avatar-action--danger:focus-visible {
|
||||||
|
color: var(--color-danger);
|
||||||
|
border-color: var(--color-danger);
|
||||||
|
background: var(--color-danger-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-avatar-action i,
|
||||||
|
.settings-avatar-action svg {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-profile-editor {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto minmax(0, 1fr);
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
gap: var(--space-4);
|
gap: var(--space-4);
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-avatar-editor__controls {
|
.settings-profile-editor__fields {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--space-2);
|
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settings-name-color-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
gap: var(--space-3);
|
||||||
|
align-items: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-name-color-row__name {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-color-field {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-color-button {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
padding: 2px;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
background: var(--color-surface);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-color-button::-webkit-color-swatch-wrapper {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-color-button::-webkit-color-swatch {
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-color-button::-moz-color-swatch {
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 520px) {
|
@media (max-width: 520px) {
|
||||||
.settings-avatar-editor {
|
.settings-profile-editor {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settings-avatar-editor {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --------------------------------------------------------
|
/* --------------------------------------------------------
|
||||||
|
|||||||
+4
-4
@@ -13,10 +13,10 @@
|
|||||||
* → bypassCacheUntil (in-memory + Cache API für SW-Restart-Robustheit)
|
* → bypassCacheUntil (in-memory + Cache API für SW-Restart-Robustheit)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const SHELL_CACHE = 'oikos-shell-v62';
|
const SHELL_CACHE = 'oikos-shell-v65';
|
||||||
const PAGES_CACHE = 'oikos-pages-v57';
|
const PAGES_CACHE = 'oikos-pages-v60';
|
||||||
const LOCALES_CACHE = 'oikos-locales-v8';
|
const LOCALES_CACHE = 'oikos-locales-v9';
|
||||||
const ASSETS_CACHE = 'oikos-assets-v57';
|
const ASSETS_CACHE = 'oikos-assets-v60';
|
||||||
const BYPASS_CACHE = 'oikos-bypass-flag';
|
const BYPASS_CACHE = 'oikos-bypass-flag';
|
||||||
const ALL_CACHES = [SHELL_CACHE, PAGES_CACHE, LOCALES_CACHE, ASSETS_CACHE];
|
const ALL_CACHES = [SHELL_CACHE, PAGES_CACHE, LOCALES_CACHE, ASSETS_CACHE];
|
||||||
|
|
||||||
|
|||||||
@@ -828,6 +828,7 @@ router.patch('/me/profile', requireAuth, csrfMiddleware, (req, res) => {
|
|||||||
const avatarData = req.body.avatar_data !== undefined
|
const avatarData = req.body.avatar_data !== undefined
|
||||||
? normalizeAvatarData(req.body.avatar_data)
|
? normalizeAvatarData(req.body.avatar_data)
|
||||||
: existing.avatar_data;
|
: existing.avatar_data;
|
||||||
|
const memberFields = validateMemberProfileFields(req.body);
|
||||||
|
|
||||||
if (!displayName) return res.status(400).json({ error: 'Display name is required.', code: 400 });
|
if (!displayName) return res.status(400).json({ error: 'Display name is required.', code: 400 });
|
||||||
if (displayName.length > 128) {
|
if (displayName.length > 128) {
|
||||||
@@ -836,6 +837,9 @@ router.patch('/me/profile', requireAuth, csrfMiddleware, (req, res) => {
|
|||||||
if (avatarData?.error) {
|
if (avatarData?.error) {
|
||||||
return res.status(400).json({ error: avatarData.error, code: 400 });
|
return res.status(400).json({ error: avatarData.error, code: 400 });
|
||||||
}
|
}
|
||||||
|
if (memberFields.errors.length) {
|
||||||
|
return res.status(400).json({ error: memberFields.errors.join(' '), code: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
db.transaction(() => {
|
db.transaction(() => {
|
||||||
db.get().prepare(`
|
db.get().prepare(`
|
||||||
@@ -845,6 +849,9 @@ router.patch('/me/profile', requireAuth, csrfMiddleware, (req, res) => {
|
|||||||
`).run(displayName, avatarColor || '#007AFF', avatarData ?? null, req.authUserId);
|
`).run(displayName, avatarColor || '#007AFF', avatarData ?? null, req.authUserId);
|
||||||
syncFamilyMemberArtifacts(db.get(), req.authUserId, {
|
syncFamilyMemberArtifacts(db.get(), req.authUserId, {
|
||||||
displayName,
|
displayName,
|
||||||
|
phone: memberFields.values.phone,
|
||||||
|
email: memberFields.values.email,
|
||||||
|
birthDate: memberFields.values.birth_date,
|
||||||
avatarData: avatarData ?? null,
|
avatarData: avatarData ?? null,
|
||||||
actorUserId: req.authUserId,
|
actorUserId: req.authUserId,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user