fix(settings): stabilize timezone edit UX and tooltip visibility (#535)
This commit is contained in:
@@ -393,7 +393,9 @@
|
||||
"select": "Zeitzone",
|
||||
"hint": "IANA-Zeitzone wählen. Wenn gesetzt, überschreibt sie die Server-TZ für deine Reminder-Zeitpunkte.",
|
||||
"useServerDefault": "Server-Standard nutzen",
|
||||
"currentServerTz": "Server-Standardzeitzone: {{timezone}}"
|
||||
"currentServerTz": "Server-Standardzeitzone: {{timezone}}",
|
||||
"saving": "Zeitzone wird gespeichert...",
|
||||
"saved": "Zeitzone gespeichert"
|
||||
},
|
||||
"apiKey": {
|
||||
"title": "API-Zugriff",
|
||||
|
||||
@@ -393,7 +393,9 @@
|
||||
"select": "Timezone",
|
||||
"hint": "Select an IANA timezone. When set, this overrides server TZ for your reminder timing.",
|
||||
"useServerDefault": "Use server default",
|
||||
"currentServerTz": "Server default timezone: {{timezone}}"
|
||||
"currentServerTz": "Server default timezone: {{timezone}}",
|
||||
"saving": "Saving timezone...",
|
||||
"saved": "Timezone saved"
|
||||
},
|
||||
"apiKey": {
|
||||
"title": "API Access",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* biome-ignore-all lint/a11y/noLabelWithoutControl: settings rows use label-styled text with adjacent custom toggle controls */
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ConfirmModal, ExportModal } from "../components";
|
||||
import { useAppContext } from "../context";
|
||||
@@ -13,8 +13,11 @@ export function SettingsPage() {
|
||||
const [apiKeyError, setApiKeyError] = useState<string | null>(null);
|
||||
const {
|
||||
settings,
|
||||
savedSettings,
|
||||
setSettings,
|
||||
settingsLoading,
|
||||
settingsSaving,
|
||||
settingsSaved,
|
||||
settingsLoadError,
|
||||
// Email testing
|
||||
testEmail,
|
||||
@@ -39,6 +42,8 @@ export function SettingsPage() {
|
||||
setImportResult,
|
||||
meds,
|
||||
} = useAppContext();
|
||||
const [timezoneTouched, setTimezoneTouched] = useState(false);
|
||||
const [timezoneDraft, setTimezoneDraft] = useState("");
|
||||
|
||||
const hasExistingData = meds.length > 0;
|
||||
let emailUnavailableReason: string | null = null;
|
||||
@@ -116,9 +121,35 @@ export function SettingsPage() {
|
||||
|
||||
const automaticStockCalculationId = "settings-stock-calculation-automatic";
|
||||
const manualStockCalculationId = "settings-stock-calculation-manual";
|
||||
|
||||
useEffect(() => {
|
||||
setTimezoneDraft(settings.timezone);
|
||||
}, [settings.timezone]);
|
||||
|
||||
const commitTimezoneDraft = () => {
|
||||
if (timezoneDraft === settings.timezone) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTimezoneTouched(true);
|
||||
setSettings((prev) => ({ ...prev, timezone: timezoneDraft }));
|
||||
};
|
||||
|
||||
const savedTimezone = savedSettings?.timezone ?? settings.timezone;
|
||||
const timezoneChanged = settings.timezone !== savedTimezone;
|
||||
const showTimezoneSaving = timezoneTouched && timezoneChanged && settingsSaving;
|
||||
const showTimezoneSaved = timezoneTouched && !timezoneChanged && settingsSaved;
|
||||
let timezoneStatusText = "";
|
||||
if (showTimezoneSaving) {
|
||||
timezoneStatusText = t("settings.timezone.saving");
|
||||
} else if (showTimezoneSaved) {
|
||||
timezoneStatusText = t("settings.timezone.saved");
|
||||
}
|
||||
const timezoneStatusClassName = showTimezoneSaved ? "timezone-status timezone-status-saved" : "timezone-status";
|
||||
const availableTimezones = Array.isArray(settings.availableTimezones) ? settings.availableTimezones : [];
|
||||
const timezoneSuggestions =
|
||||
settings.availableTimezones.length > 0
|
||||
? settings.availableTimezones
|
||||
availableTimezones.length > 0
|
||||
? availableTimezones
|
||||
: (() => {
|
||||
try {
|
||||
type IntlWithSupportedValuesOf = typeof Intl & {
|
||||
@@ -177,19 +208,28 @@ export function SettingsPage() {
|
||||
<option value="de">🇩🇪 Deutsch</option>
|
||||
</select>
|
||||
</label>
|
||||
<div className="setting-row compact" style={{ marginTop: "12px" }}>
|
||||
<div className="setting-row language-row" style={{ marginTop: "12px" }}>
|
||||
<div className="setting-label">
|
||||
<span>{t("settings.timezone.select")}</span>
|
||||
<span className="info-tooltip small" data-tooltip={t("settings.timezone.hint")}>
|
||||
<span className="info-tooltip small tooltip-align-left" data-tooltip={t("settings.timezone.hint")}>
|
||||
ⓘ
|
||||
</span>
|
||||
</div>
|
||||
<div className="setting-actions" style={{ margin: 0, flexWrap: "nowrap", gap: "8px" }}>
|
||||
<div className="setting-actions" style={{ margin: 0, flexWrap: "nowrap", gap: "8px", width: "auto" }}>
|
||||
<input
|
||||
type="text"
|
||||
className="select-field language-select"
|
||||
value={settings.timezone}
|
||||
onChange={(e) => setSettings({ ...settings, timezone: e.target.value })}
|
||||
value={timezoneDraft}
|
||||
onChange={(e) => {
|
||||
setTimezoneDraft(e.target.value);
|
||||
}}
|
||||
onBlur={commitTimezoneDraft}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
(e.currentTarget as HTMLInputElement).blur();
|
||||
}
|
||||
}}
|
||||
list="settings-timezone-suggestions"
|
||||
placeholder={settings.serverTimezone || "UTC"}
|
||||
/>
|
||||
@@ -198,11 +238,20 @@ export function SettingsPage() {
|
||||
<option key={zone} value={zone} />
|
||||
))}
|
||||
</datalist>
|
||||
<button type="button" className="ghost" onClick={() => setSettings({ ...settings, timezone: "" })}>
|
||||
<button
|
||||
type="button"
|
||||
className="ghost"
|
||||
onClick={() => {
|
||||
setTimezoneTouched(true);
|
||||
setTimezoneDraft("");
|
||||
setSettings((prev) => ({ ...prev, timezone: "" }));
|
||||
}}
|
||||
>
|
||||
{t("settings.timezone.useServerDefault")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className={timezoneStatusClassName}>{timezoneStatusText || " "}</p>
|
||||
<p className="hint-text" style={{ marginTop: "8px" }}>
|
||||
{t("settings.timezone.currentServerTz", { timezone: settings.serverTimezone || "UTC" })}
|
||||
</p>
|
||||
|
||||
Vendored
+1
-1
@@ -613,7 +613,7 @@ body.modal-open {
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr));
|
||||
margin-bottom: 1rem;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.card {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.setting-row {
|
||||
@@ -311,7 +311,7 @@
|
||||
transition:
|
||||
opacity 0.15s,
|
||||
visibility 0.15s;
|
||||
z-index: 1100;
|
||||
z-index: 12000;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -329,7 +329,7 @@
|
||||
transition:
|
||||
opacity 0.15s,
|
||||
visibility 0.15s;
|
||||
z-index: 1101;
|
||||
z-index: 12001;
|
||||
}
|
||||
|
||||
/* Tooltip aligned to left edge of icon (prevents clipping inside modals) */
|
||||
@@ -507,6 +507,20 @@
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.timezone-status {
|
||||
min-height: 1.25rem;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 0;
|
||||
padding: 0;
|
||||
font-size: 0.85rem;
|
||||
color: transparent;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.timezone-status-saved {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
/* Notification Matrix Mobile */
|
||||
@media (max-width: 480px) {
|
||||
.notification-matrix {
|
||||
|
||||
Reference in New Issue
Block a user