Compare commits
7 Commits
9bce2495b0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 475c804ce7 | |||
| 8e2fa896de | |||
| e4d771fb65 | |||
| af0809f32c | |||
| 3cc9edd4de | |||
| 0861368e4e | |||
| a27d130eaa |
@@ -22,6 +22,7 @@ dist
|
|||||||
*.log
|
*.log
|
||||||
|
|
||||||
# local env files
|
# local env files
|
||||||
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
.env.development.local
|
.env.development.local
|
||||||
.env.test.local
|
.env.test.local
|
||||||
|
|||||||
@@ -12,18 +12,30 @@ export async function PUT(
|
|||||||
const { id } = await context.params;
|
const { id } = await context.params;
|
||||||
const body: CreateWerteEntry = await request.json();
|
const body: CreateWerteEntry = await request.json();
|
||||||
|
|
||||||
const sql = `UPDATE ${TABLE} SET
|
const sql = `UPDATE ${TABLE} SET
|
||||||
Datum = '${body.Datum}',
|
Datum = ?,
|
||||||
Zeit = '${body.Zeit}',
|
Zeit = ?,
|
||||||
Zucker = ${body.Zucker || 'NULL'},
|
Zucker = ?,
|
||||||
Essen = ${body.Essen ? `'${body.Essen.replace(/'/g, "''")}'` : 'NULL'},
|
Essen = ?,
|
||||||
Gewicht = ${body.Gewicht || 'NULL'},
|
Gewicht = ?,
|
||||||
DruckS = ${body.DruckS || 'NULL'},
|
DruckS = ?,
|
||||||
DruckD = ${body.DruckD || 'NULL'},
|
DruckD = ?,
|
||||||
Puls = ${body.Puls || 'NULL'}
|
Puls = ?
|
||||||
WHERE ID = ${parseInt(id, 10)}`;
|
WHERE ID = ?`;
|
||||||
|
|
||||||
const result = await query(sql);
|
const params = [
|
||||||
|
body.Datum,
|
||||||
|
body.Zeit,
|
||||||
|
body.Zucker || null,
|
||||||
|
body.Essen || null,
|
||||||
|
body.Gewicht ? parseFloat(parseFloat(String(body.Gewicht)).toFixed(1)) : null,
|
||||||
|
body.DruckS || null,
|
||||||
|
body.DruckD || null,
|
||||||
|
body.Puls || null,
|
||||||
|
parseInt(id, 10),
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = await query(sql, params);
|
||||||
|
|
||||||
return NextResponse.json({ success: true, data: result });
|
return NextResponse.json({ success: true, data: result });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -42,8 +54,8 @@ export async function DELETE(
|
|||||||
try {
|
try {
|
||||||
const { id } = await context.params;
|
const { id } = await context.params;
|
||||||
|
|
||||||
const sql = `DELETE FROM ${TABLE} WHERE ID = ${parseInt(id, 10)}`;
|
const sql = `DELETE FROM ${TABLE} WHERE ID = ?`;
|
||||||
const result = await query(sql);
|
const result = await query(sql, [parseInt(id, 10)]);
|
||||||
|
|
||||||
return NextResponse.json({ success: true, data: result });
|
return NextResponse.json({ success: true, data: result });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ export async function GET(request: NextRequest) {
|
|||||||
let params: (string | number)[] = [];
|
let params: (string | number)[] = [];
|
||||||
|
|
||||||
if (from && to) {
|
if (from && to) {
|
||||||
sql = `SELECT ID, DATE_FORMAT(Datum, '%Y-%m-%d') as Datum, Zeit, Zucker, Essen, Gewicht, DruckD, DruckS, Puls FROM ${TABLE} WHERE Datum BETWEEN ? AND ? ORDER BY Datum ASC, Zeit ASC`;
|
sql = `SELECT ID, DATE_FORMAT(Datum, '%Y-%m-%d') as Datum, Zeit, Zucker, Essen, Gewicht, DruckS, DruckD, Puls FROM ${TABLE} WHERE Datum BETWEEN ? AND ? ORDER BY Datum ASC, Zeit ASC`;
|
||||||
params = [from, to];
|
params = [from, to];
|
||||||
} else {
|
} else {
|
||||||
sql = `SELECT ID, DATE_FORMAT(Datum, '%Y-%m-%d') as Datum, Zeit, Zucker, Essen, Gewicht, DruckD, DruckS, Puls FROM ${TABLE} ORDER BY Datum DESC, Zeit DESC LIMIT ${limit}`;
|
sql = `SELECT ID, DATE_FORMAT(Datum, '%Y-%m-%d') as Datum, Zeit, Zucker, Essen, Gewicht, DruckS, DruckD, Puls FROM ${TABLE} ORDER BY Datum DESC, Zeit DESC LIMIT ${limit}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rows = await query(sql, params);
|
const rows = await query(sql, params);
|
||||||
@@ -55,7 +55,7 @@ export async function POST(request: NextRequest) {
|
|||||||
body.Zeit,
|
body.Zeit,
|
||||||
body.Zucker || null,
|
body.Zucker || null,
|
||||||
body.Essen || null,
|
body.Essen || null,
|
||||||
body.Gewicht || null,
|
body.Gewicht ? parseFloat(parseFloat(String(body.Gewicht)).toFixed(1)) : null,
|
||||||
body.DruckS || null,
|
body.DruckS || null,
|
||||||
body.DruckD || null,
|
body.DruckD || null,
|
||||||
body.Puls || null,
|
body.Puls || null,
|
||||||
|
|||||||
@@ -15,12 +15,13 @@
|
|||||||
--font-mono: var(--font-geist-mono);
|
--font-mono: var(--font-geist-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
/*@media (prefers-color-scheme: dark) {
|
||||||
:root {
|
:root {
|
||||||
--background: #0a0a0a;
|
--background: #050505;
|
||||||
--foreground: #ededed;
|
--foreground: #ffffff;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export async function login(prevState: any, formData: FormData) {
|
|||||||
return { error: 'Bitte Benutzername und Passwort eingeben' };
|
return { error: 'Bitte Benutzername und Passwort eingeben' };
|
||||||
}
|
}
|
||||||
|
|
||||||
const isValid = verifyCredentials(username, password);
|
const isValid = await verifyCredentials(username, password);
|
||||||
|
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
return { error: 'Ungültige Anmeldedaten' };
|
return { error: 'Ungültige Anmeldedaten' };
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useActionState } from 'react';
|
import { useActionState, useState } from 'react';
|
||||||
import { login } from './actions';
|
import { login } from './actions';
|
||||||
import packageJson from '@/package.json';
|
import packageJson from '@/package.json';
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const [state, loginAction, isPending] = useActionState(login, undefined);
|
const [state, loginAction, isPending] = useActionState(login, undefined);
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
|
||||||
const version = packageJson.version;
|
const version = packageJson.version;
|
||||||
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' });
|
||||||
@@ -48,16 +49,36 @@ export default function LoginPage() {
|
|||||||
>
|
>
|
||||||
Passwort
|
Passwort
|
||||||
</label>
|
</label>
|
||||||
<input
|
<div className="relative">
|
||||||
id="password"
|
<input
|
||||||
name="password"
|
id="password"
|
||||||
type="password"
|
name="password"
|
||||||
required
|
type={showPassword ? 'text' : 'password'}
|
||||||
autoComplete="new-password"
|
required
|
||||||
className="w-full px-3 py-2 border-2 border-gray-400 rounded-lg bg-white text-gray-900 focus:border-blue-500 focus:outline-none text-sm"
|
autoComplete="new-password"
|
||||||
placeholder="Passwort"
|
className="w-full px-3 py-2 pr-10 border-2 border-gray-400 rounded-lg bg-white text-gray-900 focus:border-blue-500 focus:outline-none text-sm"
|
||||||
disabled={isPending}
|
placeholder="Passwort"
|
||||||
/>
|
disabled={isPending}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPassword(v => !v)}
|
||||||
|
tabIndex={-1}
|
||||||
|
aria-label={showPassword ? 'Passwort verbergen' : 'Passwort anzeigen'}
|
||||||
|
className="absolute inset-y-0 right-0 px-3 flex items-center text-gray-500 hover:text-gray-800"
|
||||||
|
>
|
||||||
|
{showPassword ? (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 4.411m0 0L21 21" />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{state?.error && (
|
{state?.error && (
|
||||||
|
|||||||
45
app/page.tsx
45
app/page.tsx
@@ -1,33 +1,31 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import Link from 'next/link';
|
|
||||||
import WerteForm from '@/components/WerteForm';
|
import WerteForm from '@/components/WerteForm';
|
||||||
import WerteList from '@/components/WerteList';
|
import WerteList from '@/components/WerteList';
|
||||||
import { WerteEntry } from '@/types/werte';
|
import { WerteEntry } from '@/types/werte';
|
||||||
import packageJson from '@/package.json';
|
import TabLayout from '@/components/TabLayout';
|
||||||
import LogoutButton from '@/components/LogoutButton';
|
|
||||||
|
const MAX_ENTRIES = 15
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [entries, setEntries] = useState<WerteEntry[]>([]);
|
const [entries, setEntries] = useState<WerteEntry[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [selectedEntry, setSelectedEntry] = useState<WerteEntry | null>(null);
|
const [selectedEntry, setSelectedEntry] = useState<WerteEntry | null>(null);
|
||||||
|
|
||||||
const version = packageJson.version;
|
|
||||||
const buildDate = process.env.NEXT_PUBLIC_BUILD_DATE || new Date().toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isMounted = true;
|
let isMounted = true;
|
||||||
|
|
||||||
// Fetch initial data
|
// Fetch initial data
|
||||||
(async () => {
|
(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',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.success && isMounted) {
|
if (data.success && isMounted) {
|
||||||
setEntries(data.data);
|
setEntries(data.data);
|
||||||
@@ -53,6 +51,7 @@ export default function Home() {
|
|||||||
'Cache-Control': 'no-cache',
|
'Cache-Control': 'no-cache',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setEntries(data.data);
|
setEntries(data.data);
|
||||||
@@ -81,29 +80,14 @@ export default function Home() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white py-4 px-4">
|
<TabLayout>
|
||||||
<main className="max-w-6xl mx-auto border-2 border-black rounded-lg p-6 bg-[#FFFFDD]">
|
<div className="mb-8">
|
||||||
<div className="flex justify-between items-center mb-6">
|
|
||||||
<h1 className="text-3xl font-bold">Werte - Log</h1>
|
|
||||||
<div className="flex gap-3 items-center">
|
|
||||||
<Link
|
|
||||||
href="/charts"
|
|
||||||
className="px-4 py-2 text-sm rounded-lg transition-colors shadow-md"
|
|
||||||
style={{ backgroundColor: '#374151', color: '#ffffff' }}
|
|
||||||
>
|
|
||||||
{'Verlauf ->'}
|
|
||||||
</Link>
|
|
||||||
<LogoutButton className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white text-sm rounded-lg transition-colors shadow-md" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-8">
|
|
||||||
<h2 className="text-xl font-semibold mb-4">Eingabe</h2>
|
<h2 className="text-xl font-semibold mb-4">Eingabe</h2>
|
||||||
<WerteForm onSuccess={handleSuccess} selectedEntry={selectedEntry} />
|
<WerteForm onSuccess={handleSuccess} selectedEntry={selectedEntry} />
|
||||||
</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 14 Einträge</h2>
|
<h2 className="text-xl font-semibold mb-4">Letzte {MAX_ENTRIES} Einträge</h2>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="text-center py-4">Lade Daten...</div>
|
<div className="text-center py-4">Lade Daten...</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -111,15 +95,6 @@ export default function Home() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer className="mt-8 flex justify-between items-center text-sm text-gray-600 px-4">
|
</TabLayout>
|
||||||
<a href="mailto:rxf@gmx.de" className="hover:underline">
|
|
||||||
mailto:rxf@gmx.de
|
|
||||||
</a>
|
|
||||||
<div>
|
|
||||||
Version {version} - {buildDate}
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import Link from 'next/link';
|
|
||||||
import Highcharts from 'highcharts';
|
import Highcharts from 'highcharts';
|
||||||
import HighchartsReact from 'highcharts-react-official';
|
import HighchartsReact from 'highcharts-react-official';
|
||||||
import type { Options } from 'highcharts';
|
import type { Options } from 'highcharts';
|
||||||
import { WerteEntry } from '@/types/werte';
|
import { WerteEntry } from '@/types/werte';
|
||||||
import LogoutButton from '@/components/LogoutButton';
|
import TabLayout from '@/components/TabLayout';
|
||||||
|
|
||||||
const COLOR_ZUCKER = '#e67e22';
|
const COLOR_ZUCKER = '#e67e22';
|
||||||
const COLOR_DRUCKS = '#c0392b';
|
const COLOR_DRUCKS = '#c0392b';
|
||||||
@@ -83,6 +82,7 @@ export default function ChartsClient() {
|
|||||||
cache: 'no-store',
|
cache: 'no-store',
|
||||||
headers: { 'Cache-Control': 'no-cache' },
|
headers: { 'Cache-Control': 'no-cache' },
|
||||||
});
|
});
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setEntries(data.data);
|
setEntries(data.data);
|
||||||
@@ -209,21 +209,7 @@ export default function ChartsClient() {
|
|||||||
} as Options;
|
} as Options;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white py-4 px-4">
|
<TabLayout>
|
||||||
<main className="max-w-6xl mx-auto border-2 border-black rounded-lg p-6 bg-[#FFFFDD]">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex justify-between items-center mb-6">
|
|
||||||
<h1 className="text-3xl font-bold">Werte – Verlauf</h1>
|
|
||||||
<div className="flex gap-3 items-center">
|
|
||||||
<Link
|
|
||||||
href="/"
|
|
||||||
className="bg-[#85B7D7] hover:bg-[#6a9fc5] px-4 py-2 text-sm rounded-lg transition-colors shadow-md"
|
|
||||||
>
|
|
||||||
{'<- Eingabe'}
|
|
||||||
</Link>
|
|
||||||
<LogoutButton className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white text-sm rounded-lg transition-colors shadow-md" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Date range picker */}
|
{/* Date range picker */}
|
||||||
<div className="bg-white border border-black rounded-lg p-4 mb-6 flex flex-wrap gap-4 items-end">
|
<div className="bg-white border border-black rounded-lg p-4 mb-6 flex flex-wrap gap-4 items-end">
|
||||||
@@ -283,7 +269,6 @@ export default function ChartsClient() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</main>
|
</TabLayout>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
74
components/TabLayout.tsx
Normal file
74
components/TabLayout.tsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
|
import LogoutButton from '@/components/LogoutButton';
|
||||||
|
import packageJson from '@/package.json';
|
||||||
|
|
||||||
|
interface TabLayoutProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TABS = [
|
||||||
|
{ href: '/', label: 'Eingabe' },
|
||||||
|
{ href: '/charts', label: 'Verlauf' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function TabLayout({ children }: TabLayoutProps) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const version = packageJson.version;
|
||||||
|
const buildDate = process.env.NEXT_PUBLIC_BUILD_DATE || new Date().toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen py-8 px-4">
|
||||||
|
{/* 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">
|
||||||
|
|
||||||
|
{/* Page title */}
|
||||||
|
<h1 className="text-4xl font-bold text-center mb-6 tracking-tight">Werte-Log</h1>
|
||||||
|
|
||||||
|
{/* Inner content constrained to max-w-6xl */}
|
||||||
|
<div className="max-w-6xl mx-auto">
|
||||||
|
|
||||||
|
{/* Tab bar */}
|
||||||
|
<div className="flex justify-between items-end">
|
||||||
|
<div className="flex">
|
||||||
|
{TABS.map(tab => {
|
||||||
|
const isActive = pathname === tab.href;
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={tab.href}
|
||||||
|
href={tab.href}
|
||||||
|
className="px-6 py-2 text-sm font-semibold border-t-2 border-l-2 border-r-2 rounded-tl-lg rounded-tr-lg mr-1 transition-colors"
|
||||||
|
style={
|
||||||
|
isActive
|
||||||
|
? { backgroundColor: '#FFFFDD', color: '#000000', borderColor: '#000000', borderBottom: '2px solid #FFFFDD', marginBottom: '-2px', position: 'relative', zIndex: 10 }
|
||||||
|
: { backgroundColor: '#85B7D7', color: '#374151', borderColor: '#000000' }
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="pb-1">
|
||||||
|
<LogoutButton className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white text-sm rounded-lg shadow-md" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content panel */}
|
||||||
|
<main className="border-2 border-black rounded-b-lg rounded-tr-lg p-6 bg-[#FFFFDD]">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
<footer className="mt-8 flex justify-between items-center text-sm text-gray-600 px-4">
|
||||||
|
<a href="mailto:rxf@gmx.de" className="hover:underline">
|
||||||
|
mailto:rxf@gmx.de
|
||||||
|
</a>
|
||||||
|
<div>Version {version} - {buildDate}</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -197,9 +197,10 @@ export default function WerteForm({ onSuccess, selectedEntry }: WerteFormProps)
|
|||||||
<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="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"
|
||||||
|
min={0}
|
||||||
|
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 }))}
|
||||||
maxLength={4}
|
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-2">
|
<td className="p-2">
|
||||||
@@ -216,36 +217,40 @@ export default function WerteForm({ onSuccess, selectedEntry }: WerteFormProps)
|
|||||||
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="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"
|
||||||
|
min={0}
|
||||||
|
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 }))}
|
||||||
maxLength={4}
|
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-2">
|
<td className="p-2">
|
||||||
<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="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"
|
||||||
|
min={0}
|
||||||
|
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 }))}
|
||||||
maxLength={4}
|
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-2">
|
<td className="p-2">
|
||||||
<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="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"
|
||||||
|
min={0}
|
||||||
|
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 }))}
|
||||||
maxLength={4}
|
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-2">
|
<td className="p-2">
|
||||||
<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="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"
|
||||||
|
min={0}
|
||||||
|
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 }))}
|
||||||
maxLength={4}
|
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
22
lib/auth.ts
22
lib/auth.ts
@@ -1,20 +1,10 @@
|
|||||||
/**
|
import bcrypt from 'bcryptjs';
|
||||||
* Reusable authentication library
|
|
||||||
* Configure users via environment variables in .env:
|
|
||||||
* AUTH_USERS=user1:$2a$10$hash1,user2:$2a$10$hash2
|
|
||||||
*
|
|
||||||
* Use scripts/generate-password.js to generate password hashes
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse users from environment variable
|
|
||||||
* Format: username:password,username2:password2
|
|
||||||
*/
|
|
||||||
export function getUsers(): User[] {
|
export function getUsers(): User[] {
|
||||||
const usersString = process.env.AUTH_USERS || '';
|
const usersString = process.env.AUTH_USERS || '';
|
||||||
if (!usersString) {
|
if (!usersString) {
|
||||||
@@ -30,21 +20,15 @@ export function getUsers(): User[] {
|
|||||||
.filter((user) => user.username && user.password);
|
.filter((user) => user.username && user.password);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export async function verifyCredentials(username: string, password: string): Promise<boolean> {
|
||||||
* Verify user credentials
|
|
||||||
*/
|
|
||||||
export function verifyCredentials(username: string, password: string): boolean {
|
|
||||||
const users = getUsers();
|
const users = getUsers();
|
||||||
const user = users.find(u => u.username === username);
|
const user = users.find(u => u.username === username);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return user.password === password;
|
return bcrypt.compare(password, user.password);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if authentication is enabled
|
|
||||||
*/
|
|
||||||
export function isAuthEnabled(): boolean {
|
export function isAuthEnabled(): boolean {
|
||||||
return !!process.env.AUTH_USERS;
|
return !!process.env.AUTH_USERS;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import type { QueryResult } from 'mysql2/promise';
|
|||||||
// Database configuration
|
// Database configuration
|
||||||
const dbConfig = {
|
const dbConfig = {
|
||||||
host: process.env.DB_HOST || 'mydbase_mysql',
|
host: process.env.DB_HOST || 'mydbase_mysql',
|
||||||
user: process.env.DB_USER || 'root',
|
user: process.env.DB_USER,
|
||||||
password: process.env.DB_PASS || 'SFluorit',
|
password: process.env.DB_PASS,
|
||||||
database: process.env.DB_NAME || 'RXF',
|
database: process.env.DB_NAME || 'RXF',
|
||||||
waitForConnections: true,
|
waitForConnections: true,
|
||||||
connectionLimit: 10,
|
connectionLimit: 10,
|
||||||
|
|||||||
@@ -6,6 +6,20 @@ const nextConfig: NextConfig = {
|
|||||||
turbopack: {
|
turbopack: {
|
||||||
root: path.resolve(__dirname),
|
root: path.resolve(__dirname),
|
||||||
},
|
},
|
||||||
|
async headers() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: '/(.*)',
|
||||||
|
headers: [
|
||||||
|
{ key: 'X-Frame-Options', value: 'DENY' },
|
||||||
|
{ key: 'X-Content-Type-Options', value: 'nosniff' },
|
||||||
|
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
|
||||||
|
{ key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' },
|
||||||
|
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
22
package-lock.json
generated
22
package-lock.json
generated
@@ -1,13 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "werte_next",
|
"name": "werte_next",
|
||||||
"version": "1.2.0",
|
"version": "2.0.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "werte_next",
|
"name": "werte_next",
|
||||||
"version": "1.2.0",
|
"version": "2.0.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"bcryptjs": "^3.0.3",
|
||||||
"highcharts": "^12.5.0",
|
"highcharts": "^12.5.0",
|
||||||
"highcharts-react-official": "^3.2.3",
|
"highcharts-react-official": "^3.2.3",
|
||||||
"jose": "^6.1.3",
|
"jose": "^6.1.3",
|
||||||
@@ -18,6 +19,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
@@ -1529,6 +1531,13 @@
|
|||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/bcryptjs": {
|
||||||
|
"version": "2.4.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz",
|
||||||
|
"integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||||
@@ -2461,6 +2470,15 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bcryptjs": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"bin": {
|
||||||
|
"bcrypt": "bin/bcrypt"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.12",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "werte_next",
|
"name": "werte_next",
|
||||||
"version": "2.0.0",
|
"version": "2.1.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"bcryptjs": "^3.0.3",
|
||||||
"highcharts": "^12.5.0",
|
"highcharts": "^12.5.0",
|
||||||
"highcharts-react-official": "^3.2.3",
|
"highcharts-react-official": "^3.2.3",
|
||||||
"jose": "^6.1.3",
|
"jose": "^6.1.3",
|
||||||
@@ -19,6 +20,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
|
|||||||
Reference in New Issue
Block a user