Merge branch 'responsive-mobile-fixes'

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-23 10:38:58 +02:00
6 changed files with 237 additions and 177 deletions
+7 -4
View File
@@ -6,7 +6,8 @@ import WerteList from '@/components/WerteList';
import { WerteEntry } from '@/types/werte'; import { WerteEntry } from '@/types/werte';
import TabLayout from '@/components/TabLayout'; import TabLayout from '@/components/TabLayout';
const MAX_ENTRIES = 15 const MAX_ENTRIES = 15;
const MOBILE_ENTRIES = 5;
export default function Home() { export default function Home() {
const [entries, setEntries] = useState<WerteEntry[]>([]); const [entries, setEntries] = useState<WerteEntry[]>([]);
@@ -45,7 +46,7 @@ export default function Home() {
const refreshEntries = async () => { const refreshEntries = async () => {
try { try {
const response = await fetch('/api/werte?limit=14', { const response = await fetch(`/api/werte?limit=${MAX_ENTRIES}`, {
cache: 'no-store', cache: 'no-store',
headers: { headers: {
'Cache-Control': 'no-cache', 'Cache-Control': 'no-cache',
@@ -87,11 +88,13 @@ export default function Home() {
</div> </div>
<div className="bg-white border border-black rounded-lg shadow-md p-6"> <div className="bg-white border border-black rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold mb-4">Letzte {MAX_ENTRIES} Einträge</h2> <h2 className="text-xl font-semibold mb-4">
Letzte <span className="md:hidden">{MOBILE_ENTRIES}</span><span className="hidden md:inline">{MAX_ENTRIES}</span> Einträge
</h2>
{isLoading ? ( {isLoading ? (
<div className="text-center py-4">Lade Daten...</div> <div className="text-center py-4">Lade Daten...</div>
) : ( ) : (
<WerteList entries={entries} onDelete={handleDelete} onEdit={handleEdit} /> <WerteList entries={entries} onDelete={handleDelete} onEdit={handleEdit} mobileLimit={MOBILE_ENTRIES} />
)} )}
</div> </div>
+3 -3
View File
@@ -19,9 +19,9 @@ export default function TabLayout({ children }: TabLayoutProps) {
const buildDate = process.env.NEXT_PUBLIC_BUILD_DATE || new Date().toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }); const buildDate = process.env.NEXT_PUBLIC_BUILD_DATE || new Date().toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
return ( return (
<div className="min-h-screen py-8 px-4"> <div className="min-h-screen py-8 px-2 sm:px-4">
{/* Outer wrapper: ~10% wider than the inner max-w-6xl (72rem → ~79rem), with border */} {/* Outer wrapper: ~10% wider than the inner max-w-6xl (72rem → ~79rem), with border */}
<div className="max-w-316 mx-auto border-2 border-black rounded-xl bg-gray-200 p-6"> <div className="max-w-316 mx-auto border-2 border-black rounded-xl bg-gray-200 p-3 sm:p-6">
{/* Page title */} {/* Page title */}
<h1 className="text-4xl font-bold text-center mb-6 tracking-tight">Werte-Log</h1> <h1 className="text-4xl font-bold text-center mb-6 tracking-tight">Werte-Log</h1>
@@ -51,7 +51,7 @@ export default function TabLayout({ children }: TabLayoutProps) {
</div> </div>
{/* Content panel */} {/* Content panel */}
<main className="border-2 border-black rounded-b-lg rounded-tr-lg p-6 bg-[#FFFFDD]"> <main className="border-2 border-black rounded-b-lg rounded-tr-lg p-3 sm:p-6 bg-[#FFFFDD]">
{children} {children}
</main> </main>
<footer className="mt-8 flex justify-between items-center text-sm text-gray-600 px-4"> <footer className="mt-8 flex justify-between items-center text-sm text-gray-600 px-4">
+69 -55
View File
@@ -142,122 +142,136 @@ export default function WerteForm({ onSuccess, selectedEntry }: WerteFormProps)
updateWeekday(now); updateWeekday(now);
}; };
const inputClass = "w-full min-w-0 px-2 py-1 text-sm rounded border-2 border-gray-400 bg-white focus:border-blue-500 focus:outline-none";
const numberInputClass = `${inputClass} [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none`;
// Datum/Zeit/Zucker: schmaler, damit sie bei Tablet-Hochformat (sm:) und schmalem Smartphone nicht überlappen bzw. aus dem Rahmen ragen.
// py-1.5 statt py-1 gleicht die geringere Zeilenhöhe von text-xs aus, damit die Felder gleich hoch wie die übrigen (text-sm) Felder bleiben.
const compactInputClass = "w-full min-w-0 px-1 py-1.5 text-xs rounded border-2 border-gray-400 bg-white focus:border-blue-500 focus:outline-none";
const compactNumberInputClass = `${compactInputClass} [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none`;
// Datum/Zeit: nochmal 10px schmaler als die übrigen kompakten Felder.
const dateTimeInputClass = "w-[calc(100%-10px)] min-w-0 px-1 py-1.5 text-xs rounded border-2 border-gray-400 bg-white focus:border-blue-500 focus:outline-none";
return ( return (
<div className="bg-[#CCCCFF] border border-black p-6 rounded-lg mb-6"> <div className="bg-[#CCCCFF] border border-black p-3 sm:p-6 rounded-lg mb-6">
{editId && ( {editId && (
<div className="mb-4 p-3 bg-blue-100 border border-blue-400 rounded text-sm text-blue-800"> <div className="mb-4 p-3 bg-blue-100 border border-blue-400 rounded text-sm text-blue-800">
<strong>Bearbeitungsmodus:</strong> Sie bearbeiten einen bestehenden Eintrag. Klicken Sie auf &quot;Aktualisieren&quot;, um die Änderungen zu speichern, oder &quot;Abbrechen&quot;, um zur Neuerfassung zurückzukehren. <strong>Bearbeitungsmodus:</strong> Sie bearbeiten einen bestehenden Eintrag. Klicken Sie auf &quot;Aktualisieren&quot;, um die Änderungen zu speichern, oder &quot;Abbrechen&quot;, um zur Neuerfassung zurückzukehren.
</div> </div>
)} )}
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<table className="w-full text-center"> <div className="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-8 gap-3 sm:gap-2">
<thead> <div className="flex flex-col gap-1">
<tr className="border-b-2 border-gray-400"> <label className="text-xs font-medium text-center">
<th className="p-2">Datum</th> Datum
<th className="p-2">Zeit</th> <span className="block text-[10px] font-normal">{weekday}</span>
<th className="p-2">Zucker</th> </label>
<th className="p-2">Essen</th>
<th className="p-2">Gewicht</th>
<th className="p-2">Druck sys</th>
<th className="p-2">Druck dia</th>
<th className="p-2">Puls</th>
</tr>
<tr className="border-b-2 border-gray-400">
<th className="p-2 font-normal text-sm">{weekday}</th>
<th className="p-2"></th>
<th className="p-2 font-normal text-sm">mg/dl</th>
<th className="p-2"></th>
<th className="p-2 font-normal text-sm">kg</th>
<th className="p-2 font-normal text-sm">mmHg</th>
<th className="p-2 font-normal text-sm">mmHg</th>
<th className="p-2 font-normal text-sm">bpm</th>
</tr>
</thead>
<tbody>
<tr>
<td className="p-2">
<input <input
type="date" type="date"
className="w-full px-2 py-1 text-sm rounded border-2 border-gray-400 bg-white focus:border-blue-500 focus:outline-none" className={dateTimeInputClass}
value={formData.Datum} value={formData.Datum}
onChange={(e) => handleDateChange(e.target.value)} onChange={(e) => handleDateChange(e.target.value)}
required required
/> />
</td> </div>
<td className="p-2"> <div className="flex flex-col gap-1">
<label className="text-xs font-medium text-center">
Zeit
<span className="block text-[10px] font-normal invisible">.</span>
</label>
<input <input
type="time" type="time"
className="w-full px-2 py-1 text-sm rounded border-2 border-gray-400 bg-white focus:border-blue-500 focus:outline-none" className={dateTimeInputClass}
value={formData.Zeit} value={formData.Zeit}
onChange={(e) => setFormData(prev => ({ ...prev, Zeit: e.target.value }))} onChange={(e) => setFormData(prev => ({ ...prev, Zeit: e.target.value }))}
required required
/> />
</td> </div>
<td className="p-2"> <div className="flex flex-col gap-1">
<label className="text-xs font-medium text-center">
Zucker
<span className="block text-[10px] font-normal">mg/dl</span>
</label>
<input <input
type="number" type="number"
className="w-20 px-2 py-1 text-sm rounded border-2 border-gray-400 bg-white focus:border-blue-500 focus:outline-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" className={compactNumberInputClass}
min={0} min={0}
max={999} max={999}
value={formData.Zucker} value={formData.Zucker}
onChange={(e) => setFormData(prev => ({ ...prev, Zucker: e.target.value }))} onChange={(e) => setFormData(prev => ({ ...prev, Zucker: e.target.value }))}
/> />
</td> </div>
<td className="p-2"> <div className="flex flex-col gap-1">
<label className="text-xs font-medium text-center">
Essen
<span className="block text-[10px] font-normal invisible">.</span>
</label>
<textarea <textarea
className="w-full px-2 py-1 text-sm rounded border-2 border-gray-400 bg-white focus:border-blue-500 focus:outline-none resize-none align-middle" className={`${inputClass} resize-none align-middle`}
style={{ height: '28px' }}
value={formData.Essen} value={formData.Essen}
onChange={(e) => setFormData(prev => ({ ...prev, Essen: e.target.value }))} onChange={(e) => setFormData(prev => ({ ...prev, Essen: e.target.value }))}
rows={1} rows={1}
/> />
</td> </div>
<td className="p-2"> <div className="flex flex-col gap-1">
<label className="text-xs font-medium text-center">
Gewicht
<span className="block text-[10px] font-normal">kg</span>
</label>
<input <input
type="number" type="number"
step="0.1" step="0.1"
className="w-20 px-2 py-1 text-sm rounded border-2 border-gray-400 bg-white focus:border-blue-500 focus:outline-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" className={numberInputClass}
min={0} min={0}
max={300} max={300}
value={formData.Gewicht} value={formData.Gewicht}
onChange={(e) => setFormData(prev => ({ ...prev, Gewicht: e.target.value }))} onChange={(e) => setFormData(prev => ({ ...prev, Gewicht: e.target.value }))}
/> />
</td> </div>
<td className="p-2"> <div className="flex flex-col gap-1">
<label className="text-xs font-medium text-center">
Druck sys
<span className="block text-[10px] font-normal">mmHg</span>
</label>
<input <input
type="number" type="number"
className="w-20 px-2 py-1 text-sm rounded border-2 border-gray-400 bg-white focus:border-blue-500 focus:outline-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" className={numberInputClass}
min={0} min={0}
max={300} max={300}
value={formData.DruckS} value={formData.DruckS}
onChange={(e) => setFormData(prev => ({ ...prev, DruckS: e.target.value }))} onChange={(e) => setFormData(prev => ({ ...prev, DruckS: e.target.value }))}
/> />
</td> </div>
<td className="p-2"> <div className="flex flex-col gap-1">
<label className="text-xs font-medium text-center">
Druck dia
<span className="block text-[10px] font-normal">mmHg</span>
</label>
<input <input
type="number" type="number"
className="w-20 px-2 py-1 text-sm rounded border-2 border-gray-400 bg-white focus:border-blue-500 focus:outline-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" className={numberInputClass}
min={0} min={0}
max={200} max={200}
value={formData.DruckD} value={formData.DruckD}
onChange={(e) => setFormData(prev => ({ ...prev, DruckD: e.target.value }))} onChange={(e) => setFormData(prev => ({ ...prev, DruckD: e.target.value }))}
/> />
</td> </div>
<td className="p-2"> <div className="flex flex-col gap-1">
<label className="text-xs font-medium text-center">
Puls
<span className="block text-[10px] font-normal">bpm</span>
</label>
<input <input
type="number" type="number"
className="w-20 px-2 py-1 text-sm rounded border-2 border-gray-400 bg-white focus:border-blue-500 focus:outline-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" className={numberInputClass}
min={0} min={0}
max={250} max={250}
value={formData.Puls} value={formData.Puls}
onChange={(e) => setFormData(prev => ({ ...prev, Puls: e.target.value }))} onChange={(e) => setFormData(prev => ({ ...prev, Puls: e.target.value }))}
/> />
</td> </div>
</tr> </div>
</tbody>
</table>
<div className="flex justify-center gap-4 mt-6"> <div className="flex justify-center gap-2 sm:gap-4 mt-6">
<button <button
type="submit" type="submit"
disabled={isSubmitting} disabled={isSubmitting}
+46 -3
View File
@@ -7,6 +7,7 @@ interface WerteListProps {
entries: WerteEntry[]; entries: WerteEntry[];
onDelete?: (id: number) => void; onDelete?: (id: number) => void;
onEdit?: (entry: WerteEntry) => void; onEdit?: (entry: WerteEntry) => void;
mobileLimit?: number;
} }
function ConfirmDialog({ function ConfirmDialog({
@@ -18,7 +19,7 @@ function ConfirmDialog({
}) { }) {
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40"> <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<div className="bg-white border border-gray-300 rounded-lg shadow-lg p-6 w-80 text-center"> <div className="bg-white border border-gray-300 rounded-lg shadow-lg p-6 w-80 max-w-[calc(100vw-2rem)] mx-4 text-center">
<p className="mb-6 text-gray-800 font-medium">Eintrag wirklich löschen?</p> <p className="mb-6 text-gray-800 font-medium">Eintrag wirklich löschen?</p>
<div className="flex justify-center gap-4"> <div className="flex justify-center gap-4">
<button <button
@@ -39,7 +40,7 @@ function ConfirmDialog({
); );
} }
export default function WerteList({ entries, onDelete, onEdit }: WerteListProps) { export default function WerteList({ entries, onDelete, onEdit, mobileLimit }: WerteListProps) {
const [confirmId, setConfirmId] = useState<number | null>(null); const [confirmId, setConfirmId] = useState<number | null>(null);
const formatValue = (value: number | string | null | undefined) => { const formatValue = (value: number | string | null | undefined) => {
if (value === null || value === undefined || value === '' || value === '0' || value === '0.0') { if (value === null || value === undefined || value === '' || value === '0' || value === '0.0') {
@@ -103,13 +104,14 @@ export default function WerteList({ entries, onDelete, onEdit }: WerteListProps)
} }
return ( return (
<div className="overflow-x-auto"> <div>
{confirmId !== null && ( {confirmId !== null && (
<ConfirmDialog <ConfirmDialog
onConfirm={() => handleDelete(confirmId)} onConfirm={() => handleDelete(confirmId)}
onCancel={() => setConfirmId(null)} onCancel={() => setConfirmId(null)}
/> />
)} )}
<div className="hidden md:block overflow-x-auto">
<table className="w-full border-collapse"> <table className="w-full border-collapse">
<thead> <thead>
<tr className="bg-[#CCCCFF] border-b-2 border-gray-400"> <tr className="bg-[#CCCCFF] border-b-2 border-gray-400">
@@ -167,5 +169,46 @@ export default function WerteList({ entries, onDelete, onEdit }: WerteListProps)
</tbody> </tbody>
</table> </table>
</div> </div>
<div className="md:hidden flex flex-col gap-2">
{(mobileLimit ? entries.slice(0, mobileLimit) : entries).map((entry, index) => (
<div
key={entry.ID}
className={`border border-gray-300 rounded-lg p-2 ${index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}`}
>
<div className="flex justify-between items-center text-sm font-medium">
<span>{formatDate(entry.Datum)} ({getWeekday(entry.Datum)}) · {formatTime(entry.Zeit)}</span>
{(onDelete || onEdit) && (
<div className="flex gap-2">
{onEdit && (
<button
onClick={() => onEdit(entry)}
className="text-blue-600 hover:text-blue-800 text-xs"
>
Editieren
</button>
)}
{onDelete && (
<button
onClick={() => entry.ID && setConfirmId(entry.ID)}
className="text-red-600 hover:text-red-800 text-xs"
>
Löschen
</button>
)}
</div>
)}
</div>
<div className="flex flex-wrap gap-x-3 gap-y-0.5 text-xs text-gray-700 mt-1">
<span>Zucker: {formatValue(entry.Zucker)}</span>
<span>Essen: {formatValue(entry.Essen)}</span>
<span>Gewicht: {formatValue(entry.Gewicht)}</span>
<span>RR: {formatValue(entry.DruckS)}/{formatValue(entry.DruckD)}</span>
<span>Puls: {formatValue(entry.Puls)}</span>
</div>
</div>
))}
</div>
</div>
); );
} }
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "werte_next", "name": "werte_next",
"version": "2.3.0", "version": "2.3.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "werte_next", "name": "werte_next",
"version": "2.3.0", "version": "2.3.1",
"dependencies": { "dependencies": {
"highcharts": "^12.5.0", "highcharts": "^12.5.0",
"highcharts-react-official": "^3.2.3", "highcharts-react-official": "^3.2.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "werte_next", "name": "werte_next",
"version": "2.3.0", "version": "2.3.1",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",