chore(release): v1.4.27

v1.4.27 — Mobile capability and maintainer-finding cleanup.

ResponsiveSheet primitive (bottom-sheet on narrow viewports, centred dialog on desktop)
applied to BD, Weight, Glucose, Mood, Activity forms. Maintainer-finding cleanups across
Coach, Insights, Reports, Privacy. GeoLite2 offline geocoding with resilience layer
(graceful fallback when EULA not yet propagated). E2E gap-fill coverage. 450-line
release notes attached to the GitHub Release.
This commit is contained in:
Marc-André Bombeck authored and GitHub committed 2026-05-15 22:59:10 +02:00
1 parent d98e85cd03
commit 4f09c3f862
314 files changed
+26801 -4419

No files matched your search

+80 -46
View File
@@ -1,6 +1,7 @@
"use client";
import { useState } from "react";
import { useId, useState } from "react";
import { createPortal } from "react-dom";
import { useQueryClient } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
import {
@@ -46,6 +47,15 @@ const GLP1_SIDE_EFFECT_KEYS = [
interface MoodFormProps {
onSuccess?: () => void;
onCancel?: () => void;
/**
* v1.4.27 R4 RC2 — when the form is mounted inside a
* `<ResponsiveSheet>` the caller can pass the sheet's footer slot
* element here. The action-row (kebab + Cancel + Save) is portalled
* into that slot so the bottom-sheet branch can sticky-pin it; the
* Save button stays inside the logical `<form>` via the HTML `form`
* attribute so submit-on-Enter still works.
*/
footerSlot?: HTMLElement | null;
}
function getDefaultMoodLoggedAtValue() {
@@ -55,7 +65,7 @@ function getDefaultMoodLoggedAtValue() {
return local.toISOString().slice(0, 16);
}
export function MoodForm({ onSuccess, onCancel }: MoodFormProps) {
export function MoodForm({ onSuccess, onCancel, footerSlot }: MoodFormProps) {
const { t } = useTranslations();
const queryClient = useQueryClient();
@@ -65,6 +75,15 @@ export function MoodForm({ onSuccess, onCancel }: MoodFormProps) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// v1.4.27 MB3 — error banner descriptor for the timestamp input.
const errorId = useId();
const errorDescriptor = error ? errorId : undefined;
// v1.4.27 R4 RC2 — stable form id so the portalled Save button can
// associate with this `<form>` element via the HTML `form` attribute
// even when DOM-mounted inside the `<ResponsiveSheet>` footer slot.
const formId = useId();
function resetForm() {
setMood("");
setTagsInput("");
@@ -113,8 +132,58 @@ export function MoodForm({ onSuccess, onCancel }: MoodFormProps) {
}
}
const footerNode = (
<div className="flex w-full items-center justify-between gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="button"
variant="outline"
size="icon"
className="h-9 w-9"
disabled={loading}
aria-label={t("common.moreOptions")}
>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem onClick={resetForm}>
<RotateCcw className="mr-2 h-4 w-4" />
{t("mood.formReset")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<div className="flex items-center gap-2">
{onCancel && (
<Button
type="button"
variant="outline"
onClick={onCancel}
disabled={loading}
>
{t("common.cancel")}
</Button>
)}
<Button
type="submit"
form={formId}
disabled={loading || !mood}
>
{loading ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
) : (
<Plus className="mr-2 h-4 w-4" />
)}
{t("common.save")}
</Button>
</div>
</div>
);
return (
<form onSubmit={handleSubmit} className="space-y-4">
<form id={formId} onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label id="mood-level-label">{t("mood.moodLevel")}</Label>
<div
@@ -156,6 +225,9 @@ export function MoodForm({ onSuccess, onCancel }: MoodFormProps) {
value={moodLoggedAt}
onChange={(e) => setMoodLoggedAt(e.target.value)}
required
aria-required="true"
aria-invalid={!!error || undefined}
aria-describedby={errorDescriptor}
/>
</div>
@@ -176,6 +248,9 @@ export function MoodForm({ onSuccess, onCancel }: MoodFormProps) {
value={tagsInput}
onChange={(e) => setTagsInput(e.target.value)}
placeholder={t("mood.tagsPlaceholder")}
enterKeyHint="done"
autoCapitalize="none"
autoComplete="off"
/>
{/* v1.4.25 W4d — GLP-1 side-effect quick-tags. Tapping a chip
appends the localised label to the free-text tag list.
@@ -228,6 +303,7 @@ export function MoodForm({ onSuccess, onCancel }: MoodFormProps) {
{error && (
<div
id={errorId}
role="alert"
aria-live="assertive"
className="bg-destructive/10 text-destructive rounded-lg p-3 text-sm"
@@ -236,49 +312,7 @@ export function MoodForm({ onSuccess, onCancel }: MoodFormProps) {
</div>
)}
<div className="flex items-center justify-between gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="button"
variant="outline"
size="icon"
className="h-9 w-9"
disabled={loading}
aria-label={t("common.moreOptions")}
>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem onClick={resetForm}>
<RotateCcw className="mr-2 h-4 w-4" />
{t("mood.formReset")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<div className="flex items-center gap-2">
{onCancel && (
<Button
type="button"
variant="outline"
onClick={onCancel}
disabled={loading}
>
{t("common.cancel")}
</Button>
)}
<Button type="submit" disabled={loading || !mood}>
{loading ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<Plus className="mr-2 h-4 w-4" />
)}
{t("common.save")}
</Button>
</div>
</div>
{footerSlot ? createPortal(footerNode, footerSlot) : footerNode}
</form>
);
}
+89 -82
View File
@@ -23,12 +23,7 @@ import {
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
import {
DropdownMenu,
DropdownMenuContent,
@@ -59,26 +54,20 @@ import {
ArrowUpDown,
MoreHorizontal,
} from "lucide-react";
import { useState } from "react";
import { useId, useState } from "react";
import { createPortal } from "react-dom";
import { formatDateTime } from "@/lib/format";
import { useTranslations, useFormatters } from "@/lib/i18n/context";
import {
MOOD_LABEL_KEYS,
MOOD_SCORE_BY_ENUM,
} from "@/lib/mood/labels";
import { invalidateKeys, moodDependentKeys } from "@/lib/query-keys";
const MOOD_SCORES: Record<string, number> = {
SUPER_GUT: 5,
GUT: 4,
OKAY: 3,
SCHLECHT: 2,
LAUSIG: 1,
};
const MOOD_LABEL_KEYS: Record<string, string> = {
SUPER_GUT: "mood.levelSuperGut",
GUT: "mood.levelGut",
OKAY: "mood.levelOkay",
SCHLECHT: "mood.levelSchlecht",
LAUSIG: "mood.levelLausig",
};
// Re-export the score map under the legacy local name to keep the
// rest of this file unchanged. v1.4.27 B6 / BL-P6-11 — the single
// source of truth now lives in `@/lib/mood/labels`.
const MOOD_SCORES = MOOD_SCORE_BY_ENUM as Record<string, number>;
interface MoodEntry {
id: string;
@@ -133,6 +122,11 @@ export function MoodList({ onAddFirst }: MoodListProps = {}) {
const [editMoodLoggedAt, setEditMoodLoggedAt] = useState("");
const [editError, setEditError] = useState<string | null>(null);
const [editDeleteDialogOpen, setEditDeleteDialogOpen] = useState(false);
// v1.4.27 R4 RC2 — Sheet-branch sticky-pinned footer slot.
const editFormId = useId();
const [editFooterEl, setEditFooterEl] = useState<HTMLDivElement | null>(
null,
);
const setMoodFilter = (value: string) => {
setMoodFilterRaw(value);
@@ -524,14 +518,19 @@ export function MoodList({ onAddFirst }: MoodListProps = {}) {
)}
</div>
{/* Edit dialog */}
<Dialog open={!!editing} onOpenChange={(open) => !open && closeEdit()}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t("mood.editEntry")}</DialogTitle>
</DialogHeader>
{/* Edit sheet — bottom-sheet on `<md`, centred Dialog on `md+`. */}
<ResponsiveSheet
open={!!editing}
onOpenChange={(open) => !open && closeEdit()}
title={t("mood.editEntry")}
footer={<div ref={setEditFooterEl} className="flex w-full" />}
>
{editing && (
<form onSubmit={submitEdit} className="space-y-4">
<form
id={editFormId}
onSubmit={submitEdit}
className="space-y-4"
>
<div className="space-y-2">
<Label id="edit-mood-level-label">{t("mood.moodLevel")}</Label>
<div
@@ -605,57 +604,66 @@ export function MoodList({ onAddFirst }: MoodListProps = {}) {
</div>
)}
<div className="flex items-center justify-between gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="button"
variant="outline"
size="icon"
className="h-9 w-9"
disabled={
updateMutation.isPending || deleteMutation.isPending
}
aria-label={t("common.moreOptions")}
>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem
variant="destructive"
onClick={() => setEditDeleteDialogOpen(true)}
>
<Trash2 className="mr-2 h-4 w-4" />
{t("common.delete")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{editFooterEl
? createPortal(
<div className="flex w-full items-center justify-between gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="button"
variant="outline"
size="icon"
className="h-9 w-9"
disabled={
updateMutation.isPending ||
deleteMutation.isPending
}
aria-label={t("common.moreOptions")}
>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem
variant="destructive"
onClick={() => setEditDeleteDialogOpen(true)}
>
<Trash2 className="mr-2 h-4 w-4" />
{t("common.delete")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<div className="flex items-center gap-2">
<Button
type="button"
variant="outline"
onClick={closeEdit}
disabled={
updateMutation.isPending || deleteMutation.isPending
}
>
{t("common.cancel")}
</Button>
<Button
type="submit"
disabled={
updateMutation.isPending || deleteMutation.isPending
}
>
{updateMutation.isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : null}
{t("common.save")}
</Button>
</div>
</div>
<div className="flex items-center gap-2">
<Button
type="button"
variant="outline"
onClick={closeEdit}
disabled={
updateMutation.isPending ||
deleteMutation.isPending
}
>
{t("common.cancel")}
</Button>
<Button
type="submit"
form={editFormId}
disabled={
updateMutation.isPending ||
deleteMutation.isPending
}
>
{updateMutation.isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
) : null}
{t("common.save")}
</Button>
</div>
</div>,
editFooterEl,
)
: null}
<AlertDialog
open={editDeleteDialogOpen}
@@ -680,7 +688,7 @@ export function MoodList({ onAddFirst }: MoodListProps = {}) {
disabled={deleteMutation.isPending}
>
{deleteMutation.isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
) : null}
{t("common.delete")}
</AlertDialogAction>
@@ -689,8 +697,7 @@ export function MoodList({ onAddFirst }: MoodListProps = {}) {
</AlertDialog>
</form>
)}
</DialogContent>
</Dialog>
</ResponsiveSheet>
</>
);
}